enabling linter

This commit is contained in:
otsmr 2025-07-15 00:38:23 +02:00
parent 0eda03537c
commit a3d9bcf98b
148 changed files with 4643 additions and 4406 deletions

271
.vscode/launch.json vendored
View file

@ -1,271 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "twonly-app",
"request": "launch",
"type": "dart"
},
{
"name": "twonly-app (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "twonly-app (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage",
"cwd": "dependencies/flutter_secure_storage",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_zxing",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_zxing (profile mode)",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_zxing (release mode)",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_darwin",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_darwin (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_darwin (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_linux",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_linux (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_linux (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_platform_interface",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_platform_interface (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_platform_interface (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_web",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_web (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_web (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_windows",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_windows (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_windows (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "zxscanner",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart"
},
{
"name": "zxscanner (profile mode)",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "zxscanner (release mode)",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_macos",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_macos (profile mode)",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_macos (release mode)",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View file

@ -1,5 +0,0 @@
{
"files.exclude": {
"dependencies": false
}
}

View file

@ -13,12 +13,15 @@ This repository contains the complete source code of the [twonly](https://twonly
## In work ## In work
- We plan to implement a Sealed Sender feature to minimize metadata - For Android: Using [UnifiedPush](https://unifiedpush.org/) instead of FCM
- We currently evaluating to switch from the Signal Protocol to [MLS](https://openmls.tech/). - For Android: Reproducible Builds + Publishing on Github/F-Droid
- Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata
- Maybe: Switching from the Signal Protocol to [MLS](https://openmls.tech/).
## Security Issues ## Security Issues
If you discover a security issue in twonly, please adhere to the coordinated vulnerability disclosure model. Please send us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not guarantee a bounty currently :/ If you discover a security issue in twonly, please adhere to the coordinated vulnerability disclosure model. Please send
us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not
guarantee a bounty currently :/
## Development ## Development

View file

@ -1,28 +1,24 @@
# This file configures the analyzer, which statically analyzes Dart code to include: package:very_good_analysis/analysis_options.yaml
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps, analyzer:
# packages, and plugins designed to encourage good coding practices. errors:
include: package:flutter_lints/flutter.yaml always_use_package_imports: ignore
public_member_api_docs: ignore
lines_longer_than_80_chars: ignore
avoid_function_literals_in_foreach_calls: ignore
avoid_setters_without_getters: ignore
inference_failure_on_instance_creation: ignore
avoid_positional_boolean_parameters: ignore
inference_failure_on_collection_literal: ignore
exclude:
- "lib/src/model/protobuf/**"
- "lib/src/model/protobuf/api/websocket/**"
- "lib/generated/**"
- "test/drift/**"
- "**.g.dart"
linter: linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule public_member_api_docs: false
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule avoid_catches_without_on_clauses: false
document_ignores: false
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -1,175 +0,0 @@
emojies = {
"": "red_heart.json",
"😂": "joy.json",
"🔥": "fire.json",
"💪": "muscle.json",
"😭": "loudly-crying.json",
"🤯": "mind-blown.json",
"❤️‍🔥": "red_heart_fire.json",
"😁": "grinning.json",
"😆": "laughing.json",
"😅": "grin-sweat.json",
"🤣": "rofl.json",
"😉": "wink.json",
"😘": "kissing-heart.json",
"🥰": "heart-face.json",
"😍": "heart-eyes.json",
"🤩": "star-struck.json",
"🥳": "partying-face.json",
"🙃": "upside-down-face.json",
"🥲": "happy-cry.json",
"😊": "blush.json",
"😏": "smirk.json",
"🤤": "drool.json",
"😋": "yum.json",
"😛": "stuck-out-tongue.json",
"🤪": "zany-face.json",
"🥴": "woozy.json",
"😔": "pensive.json",
"🥺": "pleading.json",
"😬": "grimacing.json",
"😑": "expressionless.json",
"🤐": "zipper-face.json",
"🤔": "thinking-face.json",
"🥱": "yawn.json",
"🤗": "hug-face.json",
"😱": "screaming.json",
"🤨": "raised-eyebrow.json",
"🧐": "monocle.json",
"😒": "unamused.json",
"🙄": "rolling-eyes.json",
"😤": "triumph.json",
"🤬": "cursing.json",
"😞": "sad.json",
"😢": "cry.json",
"🙁": "frown.json",
"😨": "scared.json",
"😳": "flushed.json",
"😖": "scrunched-mouth.json",
"😵": "x-eyes.json",
"🥶": "cold-face.json",
"🥵": "hot-face.json",
"🤮": "vomit.json",
"😴": "sleep.json",
"🤒": "thermometer-face.json",
"🤕": "bandage-face.json",
"🤥": "liar.json",
"😇": "halo.json",
"🤠": "cowboy.json",
"🤑": "money-face.json",
"🤓": "nerd-face.json",
"😎": "sunglasses-face.json",
"🥸": "disguise.json",
"🤡": "clown.json",
"💩": "poop.json",
"😈": "imp-smile.json",
"👻": "ghost.json",
"💀": "skull.json",
"": "snowman.json",
"🎃": "jack-o-lantern.json",
"🤖": "robot.json",
"👽": "alien.json",
"🙈": "see-no-evil-monkey.json",
"🙉": "hear-no-evil-monkey.json",
"🙊": "speak-no-evil-monkey.json",
"🌟": "glowing-star.json",
"": "sparkles.json",
"": "electricity.json",
"💥": "collision.json",
"💯": "100.json",
"🎉": "party-popper.json",
"🎊": "confetti-ball.json",
"🧡": "orange-heart.json",
"💛": "yellow-heart.json",
"💚": "green-heart.json",
"💙": "blue-heart.json",
"💜": "purple-heart.json",
"💘": "cupid.json",
"💝": "gift-heart.json",
"💖": "sparkling-heart.json",
"💕": "two-hearts.json",
"💔": "broken-heart.json",
"💋": "kiss.json",
"👀": "eyes.json",
"🦻": "hearing-aid.json",
"🦶": "foot.json",
"🦾": "arm-mechanical.json",
"👏": "clap.json",
"👍": "thumbs-up.json",
"👎": "thumbs-down.json",
"🙌": "raising-hands.json",
"": "raised-fist.json",
"👊": "fist.json",
"👋": "wave.json",
"🤘": "metal.json",
"🤞": "crossed-fingers.json",
"🤙": "call-me-hand.json",
"👌": "ok.json",
"🖕": "middle-finger.json",
"🤝": "handshake.json",
"💃": "dancer-woman.json",
"🌱": "plant.json",
"🍃": "leaves.json",
"🍀": "luck.json",
"🌊": "ocean.json",
"💧": "droplet.json",
"🦄": "unicorn.json",
"🦖": "t-rex.json",
"🦕": "dinosaur.json",
"🐢": "turtle.json",
"🐍": "snake.json",
"🐩": "poodle.json",
"🐕": "dog.json",
"🐖": "pig.json",
"🦘": "kangaroo.json",
"🦍": "gorilla.json",
"🦧": "orangutan.json",
"🦦": "otter.json",
"🐓": "rooster.json",
"🦅": "eagle.json",
"🦉": "owl.json",
"🐬": "dolphin.json",
"🐳": "whale.json",
"🐟": "fish.json",
"🐡": "blowfish.json",
"🦀": "crab.json",
"🐙": "octopus.json",
"🐌": "snail.json",
"🍻": "clinking-beer-mugs.json",
"🍾": "bottle-with-popping-cork.json",
"🚨": "police-car-light.json",
"🛸": "flying-saucer.json",
"🚀": "rocket.json",
"🛫": "airplane-departure.json",
"🎢": "roller-coaster.json",
"🎡": "ferris-wheel.json",
"🎈": "balloon.json",
"🎁": "wrapped-gift.json",
"🎆": "fireworks.json",
"💸": "money-with-wings.json",
"💎": "gem-stone.json",
"🎓": "graduation-cap.json",
"🔔": "bell.json",
"💣": "bomb.json",
"": "exclamation.json",
"": "question.json",
"": "cross-mark.json",
"🏁": "chequered-flag.json",
"🚩": "triangular-flag.json",
"🏴": "black-flag.json",
}
import os
files = [f for f in os.listdir(".") if os.path.isfile(os.path.join(".", f))]
for file in files:
if ".json" in file:
found = False
for name in emojies.values():
if name == file:
found = True
if not found:
print("DELETE", file)
os.remove(file)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,24 +1,18 @@
import 'dart:io'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart'; import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'dart:async';
import 'package:url_launcher/url_launcher.dart';
// these two callbacks are called on updated to the corresponding database
/// The Widget that configures your application.
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key}); const App({super.key});
@override @override
@ -27,7 +21,6 @@ class App extends StatefulWidget {
class _AppState extends State<App> with WidgetsBindingObserver { class _AppState extends State<App> with WidgetsBindingObserver {
bool wasPaused = false; bool wasPaused = false;
bool appIsOutdated = false;
@override @override
void initState() { void initState() {
@ -35,16 +28,15 @@ class _AppState extends State<App> with WidgetsBindingObserver {
globalIsAppInBackground = false; globalIsAppInBackground = false;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
// register global callbacks to the widget tree globalCallbackConnectionState = ({required bool isConnected}) {
globalCallbackConnectionState = (update) { context.read<CustomChangeProvider>().updateConnectionState(isConnected);
context.read<CustomChangeProvider>().updateConnectionState(update);
setUserPlan(); setUserPlan();
}; };
initAsync(); initAsync();
} }
Future setUserPlan() async { Future<void> setUserPlan() async {
final user = await getUser(); final user = await getUser();
globalBestFriendUserId = -1; globalBestFriendUserId = -1;
if (user != null && mounted) { if (user != null && mounted) {
@ -59,23 +51,19 @@ class _AppState extends State<App> with WidgetsBindingObserver {
} }
} }
if (mounted) { if (mounted) {
context.read<CustomChangeProvider>().updatePlan(user.subscriptionPlan); await context
.read<CustomChangeProvider>()
.updatePlan(user.subscriptionPlan);
} }
} }
} }
Future initAsync() async { Future<void> initAsync() async {
setUserPlan(); await setUserPlan();
globalCallbackAppIsOutdated = () async {
context.read<CustomChangeProvider>().updateConnectionState(false);
setState(() {
appIsOutdated = true;
});
};
await apiService.connect(force: true); await apiService.connect(force: true);
apiService.listenToNetworkChanges(); await apiService.listenToNetworkChanges();
// call this function so invalid media files are get purged // call this function so invalid media files are get purged
retryMediaUpload(true); await retryMediaUpload(true);
} }
@override @override
@ -97,8 +85,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
globalCallbackConnectionState = (a) {}; globalCallbackConnectionState = ({required bool isConnected}) {};
globalCallbackAppIsOutdated = () {};
super.dispose(); super.dispose();
} }
@ -145,10 +132,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
themeMode: context.watch<SettingsChangeProvider>().themeMode, themeMode: context.watch<SettingsChangeProvider>().themeMode,
initialRoute: '/', initialRoute: '/',
routes: { routes: {
"/": (context) => "/": (context) => AppMainWidget(initialPage: 1),
AppMainWidget(initialPage: 1, appIsOutdated: appIsOutdated), "/chats": (context) => AppMainWidget(initialPage: 0)
"/chats": (context) =>
AppMainWidget(initialPage: 0, appIsOutdated: appIsOutdated)
}, },
); );
}, },
@ -157,17 +142,18 @@ class _AppState extends State<App> with WidgetsBindingObserver {
} }
class AppMainWidget extends StatefulWidget { class AppMainWidget extends StatefulWidget {
const AppMainWidget( const AppMainWidget({
{super.key, required this.initialPage, required this.appIsOutdated}); super.key,
required this.initialPage,
});
final int initialPage; final int initialPage;
final bool appIsOutdated;
@override @override
State<AppMainWidget> createState() => _AppMainWidgetState(); State<AppMainWidget> createState() => _AppMainWidgetState();
} }
class _AppMainWidgetState extends State<AppMainWidget> { class _AppMainWidgetState extends State<AppMainWidget> {
Future<bool> userCreated = isUserCreated(); Future<bool> userCreated = isUserCreated();
bool showOnboarding = kReleaseMode; bool showOnboarding = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -178,80 +164,25 @@ class _AppMainWidgetState extends State<AppMainWidget> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Center(child: Container()); return Center(child: Container());
} } else if (snapshot.data!) {
if (snapshot.data!) {
return HomeView( return HomeView(
initialPage: widget.initialPage, initialPage: widget.initialPage,
); );
} } else if (showOnboarding) {
return OnboardingView(
return showOnboarding callbackOnSuccess: () => setState(() {
? OnboardingView(
callbackOnSuccess: () {
setState(() {
showOnboarding = false; showOnboarding = false;
}); }),
}, );
) }
: RegisterView( return RegisterView(
callbackOnSuccess: () { callbackOnSuccess: () => setState(() {
setState(() {
userCreated = isUserCreated(); userCreated = isUserCreated();
}); }),
},
); );
}, },
), ),
if (widget.appIsOutdated) AppOutdated(),
Positioned(
top: 60,
left: 30,
right: 30,
child: SafeArea(
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 8),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
context.lang.appOutdated,
textAlign: TextAlign.center,
softWrap: true,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
if (Platform.isAndroid) SizedBox(height: 5),
if (Platform.isAndroid)
ElevatedButton(
onPressed: () {
launchUrl(Uri.parse(
"https://play.google.com/store/apps/details?id=eu.twonly"));
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
context.lang.appOutdatedBtn,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
),
],
),
),
),
),
], ],
); );
} }

View file

@ -15,8 +15,10 @@ bool gIsDemoUser = false;
// App widget. // App widget.
// This callback called by the apiProvider // This callback called by the apiProvider
Function(bool) globalCallbackConnectionState = (a) {}; void Function({required bool isConnected}) globalCallbackConnectionState = ({
Function() globalCallbackAppIsOutdated = () {}; required bool isConnected,
}) {};
void Function() globalCallbackAppIsOutdated = () {};
bool globalIsAppInBackground = true; bool globalIsAppInBackground = true;
int globalBestFriendUserId = -1; int globalBestFriendUserId = -1;

View file

@ -1,20 +1,23 @@
import 'dart:async';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'app.dart'; import 'app.dart';
void main() async { void main() async {
@ -31,31 +34,29 @@ void main() async {
} }
final settingsController = SettingsChangeProvider(); final settingsController = SettingsChangeProvider();
// Load the user's preferred theme while the splash screen is displayed.
// This prevents a sudden theme change when the app is first displayed.
await settingsController.loadSettings(); await settingsController.loadSettings();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
setupPushNotification(); unawaited(setupPushNotification());
gCameras = await availableCameras(); gCameras = await availableCameras();
apiService = ApiService(); apiService = ApiService();
twonlyDB = TwonlyDatabase(); twonlyDB = TwonlyDatabase();
await twonlyDB.messagesDao.resetPendingDownloadState(); await twonlyDB.messagesDao.resetPendingDownloadState();
await twonlyDB.messagesDao.handleMediaFilesOlderThan7Days(); await twonlyDB.messagesDao.handleMediaFilesOlderThan7Days();
await twonlyDB.signalDao.purgeOutDatedPreKeys(); await twonlyDB.signalDao.purgeOutDatedPreKeys();
// Purge media files in the background // Purge media files in the background
purgeReceivedMediaFiles(); unawaited(purgeReceivedMediaFiles());
purgeSendMediaFiles(); unawaited(purgeSendMediaFiles());
performTwonlySafeBackup(); unawaited(performTwonlySafeBackup());
await initFileDownloader(); await initFileDownloader();
cleanLogFile();
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
@ -63,7 +64,7 @@ void main() async {
ChangeNotifierProvider(create: (_) => CustomChangeProvider()), ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
ChangeNotifierProvider(create: (_) => ImageEditorProvider()), ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
], ],
child: App(), child: const App(),
), ),
); );
} }

View file

@ -1,11 +1,11 @@
class SecureStorageKeys { class SecureStorageKeys {
static const String signalIdentity = "signal_identity"; static const String signalIdentity = 'signal_identity';
static const String signalSignedPreKey = "signed_pre_key_store"; static const String signalSignedPreKey = 'signed_pre_key_store';
static const String apiAuthToken = "api_auth_token"; static const String apiAuthToken = 'api_auth_token';
static const String googleFcm = "google_fcm"; static const String googleFcm = 'google_fcm';
static const String userData = "userData"; static const String userData = 'userData';
static const String twonlySafeLastBackupHash = "twonly_safe_last_backup_hash"; static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash';
static const String receivingPushKeys = "receiving_push_keys"; static const String receivingPushKeys = 'receiving_push_keys';
static const String sendingPushKeys = "sending_push_keys"; static const String sendingPushKeys = 'sending_push_keys';
} }

View file

@ -20,30 +20,33 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
} }
} }
Future incFlameCounter( Future<int> incFlameCounter(
int contactId, bool received, DateTime timestamp) async { int contactId,
Contact contact = (await (select(contacts) bool received,
DateTime timestamp,
) async {
final contact = (await (select(contacts)
..where((t) => t.userId.equals(contactId))) ..where((t) => t.userId.equals(contactId)))
.get()) .get())
.first; .first;
int totalMediaCounter = contact.totalMediaCounter + 1; final totalMediaCounter = contact.totalMediaCounter + 1;
int flameCounter = contact.flameCounter; var flameCounter = contact.flameCounter;
if (contact.lastMessageReceived != null && if (contact.lastMessageReceived != null &&
contact.lastMessageSend != null) { contact.lastMessageSend != null) {
final now = DateTime.now(); final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day); final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(Duration(days: 2)); final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
if (contact.lastMessageSend!.isBefore(twoDaysAgo) || if (contact.lastMessageSend!.isBefore(twoDaysAgo) ||
contact.lastMessageReceived!.isBefore(twoDaysAgo)) { contact.lastMessageReceived!.isBefore(twoDaysAgo)) {
flameCounter = 0; flameCounter = 0;
} }
} }
Value<DateTime?> lastMessageSend = Value.absent(); var lastMessageSend = const Value<DateTime?>.absent();
Value<DateTime?> lastMessageReceived = Value.absent(); var lastMessageReceived = const Value<DateTime?>.absent();
Value<DateTime?> lastFlameCounterChange = Value.absent(); var lastFlameCounterChange = const Value<DateTime?>.absent();
if (contact.lastFlameCounterChange != null) { if (contact.lastFlameCounterChange != null) {
final now = DateTime.now(); final now = DateTime.now();
@ -51,7 +54,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
if (contact.lastFlameCounterChange!.isBefore(startOfToday)) { if (contact.lastFlameCounterChange!.isBefore(startOfToday)) {
// last flame update was yesterday. check if it can be updated. // last flame update was yesterday. check if it can be updated.
bool updateFlame = false; var updateFlame = false;
if (received) { if (received) {
if (contact.lastMessageSend != null && if (contact.lastMessageSend != null &&
contact.lastMessageSend!.isAfter(startOfToday)) { contact.lastMessageSend!.isAfter(startOfToday)) {
@ -94,11 +97,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
return select(contacts)..where((t) => t.userId.equals(userId)); return select(contacts)..where((t) => t.userId.equals(userId));
} }
Future deleteContactByUserId(int userId) { Future<void> deleteContactByUserId(int userId) {
return (delete(contacts)..where((t) => t.userId.equals(userId))).go(); return (delete(contacts)..where((t) => t.userId.equals(userId))).go();
} }
Future updateContact(int userId, ContactsCompanion updatedValues) async { Future<void> updateContact(
int userId, ContactsCompanion updatedValues) async {
await ((update(contacts)..where((c) => c.userId.equals(userId))) await ((update(contacts)..where((c) => c.userId.equals(userId)))
.write(updatedValues)); .write(updatedValues));
if (updatedValues.blocked.present || if (updatedValues.blocked.present ||
@ -111,7 +115,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
} }
} }
Future newMessageExchange(int userId) { Future<void> newMessageExchange(int userId) {
return updateContact( return updateContact(
userId, userId,
ContactsCompanion( ContactsCompanion(

View file

@ -11,7 +11,7 @@ class MediaDownloadsDao extends DatabaseAccessor<TwonlyDatabase>
with _$MediaDownloadsDaoMixin { with _$MediaDownloadsDaoMixin {
MediaDownloadsDao(super.db); MediaDownloadsDao(super.db);
Future updateMediaDownload( Future<void> updateMediaDownload(
int messageId, MediaDownloadsCompanion updatedValues) { int messageId, MediaDownloadsCompanion updatedValues) {
return (update(mediaDownloads)..where((c) => c.messageId.equals(messageId))) return (update(mediaDownloads)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues); .write(updatedValues);
@ -26,7 +26,7 @@ class MediaDownloadsDao extends DatabaseAccessor<TwonlyDatabase>
} }
} }
Future deleteMediaDownload(int messageId) { Future<void> deleteMediaDownload(int messageId) {
return (delete(mediaDownloads)..where((t) => t.messageId.equals(messageId))) return (delete(mediaDownloads)..where((t) => t.messageId.equals(messageId)))
.go(); .go();
} }

View file

@ -33,7 +33,7 @@ class MediaUploadsDao extends DatabaseAccessor<TwonlyDatabase>
} }
} }
Future deleteMediaUpload(int mediaUploadId) { Future<void> deleteMediaUpload(int mediaUploadId) {
return (delete(mediaUploads) return (delete(mediaUploads)
..where((t) => t.mediaUploadId.equals(mediaUploadId))) ..where((t) => t.mediaUploadId.equals(mediaUploadId)))
.go(); .go();

View file

@ -45,7 +45,7 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
..where((t) => t.retransmissionId.equals(retransmissionId)); ..where((t) => t.retransmissionId.equals(retransmissionId));
} }
Future updateRetransmission( Future<void> updateRetransmission(
int retransmissionId, int retransmissionId,
MessageRetransmissionsCompanion updatedValues, MessageRetransmissionsCompanion updatedValues,
) { ) {
@ -54,13 +54,13 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
.write(updatedValues); .write(updatedValues);
} }
Future resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async { Future<int> resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async {
return ((update(messageRetransmissions)) return ((update(messageRetransmissions))
..where((m) => ..where((m) =>
m.contactId.equals(fromUserId) & m.contactId.equals(fromUserId) &
m.encryptedHash.equals(encryptedHash))) m.encryptedHash.equals(encryptedHash)))
.write( .write(
MessageRetransmissionsCompanion( const MessageRetransmissionsCompanion(
acknowledgeByServerAt: Value(null), acknowledgeByServerAt: Value(null),
), ),
); );
@ -75,13 +75,13 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
.getSingleOrNull(); .getSingleOrNull();
} }
Future deleteRetransmissionById(int retransmissionId) { Future<void> deleteRetransmissionById(int retransmissionId) {
return (delete(messageRetransmissions) return (delete(messageRetransmissions)
..where((t) => t.retransmissionId.equals(retransmissionId))) ..where((t) => t.retransmissionId.equals(retransmissionId)))
.go(); .go();
} }
Future deleteRetransmissionByMessageId(int messageId) { Future<void> deleteRetransmissionByMessageId(int messageId) {
return (delete(messageRetransmissions) return (delete(messageRetransmissions)
..where((t) => t.messageId.equals(messageId))) ..where((t) => t.messageId.equals(messageId)))
.go(); .go();

View file

@ -56,7 +56,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.watch(); .watch();
} }
Future removeOldMessages() { Future<void> removeOldMessages() {
return (update(messages) return (update(messages)
..where((t) => ..where((t) =>
(t.openedAt.isSmallerThanValue( (t.openedAt.isSmallerThanValue(
@ -69,7 +69,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(MessagesCompanion(contentJson: Value(null))); .write(MessagesCompanion(contentJson: Value(null)));
} }
Future handleMediaFilesOlderThan7Days() { Future<void> handleMediaFilesOlderThan7Days() {
/// media files will be deleted by the server after 7 days, so delete them here also /// media files will be deleted by the server after 7 days, so delete them here also
return (update(messages) return (update(messages)
..where( ..where(
@ -130,7 +130,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.get(); .get();
} }
Future openedAllNonMediaMessages(int contactId) { Future<void> openedAllNonMediaMessages(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now())); final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages) return (update(messages)
..where((t) => ..where((t) =>
@ -141,7 +141,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updates); .write(updates);
} }
Future resetPendingDownloadState() { Future<void> resetPendingDownloadState() {
// All media files in the downloading state are reseteded to the pending state // All media files in the downloading state are reseteded to the pending state
// When the app is used in mobile network, they will not be downloaded at the start // When the app is used in mobile network, they will not be downloaded at the start
// if they are not yet downloaded... // if they are not yet downloaded...
@ -155,7 +155,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updates); .write(updates);
} }
Future openedAllNonMediaMessagesFromOtherUser(int contactId) { Future<void> openedAllNonMediaMessagesFromOtherUser(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now())); final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages) return (update(messages)
..where((t) => ..where((t) =>
@ -167,7 +167,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updates); .write(updates);
} }
Future updateMessageByOtherUser( Future<void> updateMessageByOtherUser(
int userId, int messageId, MessagesCompanion updatedValues) { int userId, int messageId, MessagesCompanion updatedValues) {
return (update(messages) return (update(messages)
..where((c) => ..where((c) =>
@ -175,7 +175,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updatedValues); .write(updatedValues);
} }
Future updateMessageByOtherMessageId( Future<void> updateMessageByOtherMessageId(
int userId, int messageOtherId, MessagesCompanion updatedValues) { int userId, int messageOtherId, MessagesCompanion updatedValues) {
return (update(messages) return (update(messages)
..where((c) => ..where((c) =>
@ -184,7 +184,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updatedValues); .write(updatedValues);
} }
Future updateMessageByMessageId( Future<void> updateMessageByMessageId(
int messageId, MessagesCompanion updatedValues) { int messageId, MessagesCompanion updatedValues) {
return (update(messages)..where((c) => c.messageId.equals(messageId))) return (update(messages)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues); .write(updatedValues);
@ -203,14 +203,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
} }
} }
Future deleteMessagesByContactId(int contactId) { Future<void> deleteMessagesByContactId(int contactId) {
return (delete(messages) return (delete(messages)
..where((t) => ..where((t) =>
t.contactId.equals(contactId) & t.mediaStored.equals(false))) t.contactId.equals(contactId) & t.mediaStored.equals(false)))
.go(); .go();
} }
Future deleteMessagesByContactIdAndOtherMessageId( Future<void> deleteMessagesByContactIdAndOtherMessageId(
int contactId, int messageOtherId) { int contactId, int messageOtherId) {
return (delete(messages) return (delete(messages)
..where((t) => ..where((t) =>
@ -219,11 +219,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.go(); .go();
} }
Future deleteMessagesByMessageId(int messageId) { Future<void> deleteMessagesByMessageId(int messageId) {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
} }
Future deleteAllMessagesByContactId(int contactId) { Future<void> deleteAllMessagesByContactId(int contactId) {
return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); return (delete(messages)..where((t) => t.contactId.equals(contactId))).go();
} }

View file

@ -15,7 +15,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
// of this object. // of this object.
SignalDao(super.db); SignalDao(super.db);
Future deleteAllByContactId(int contactId) async { Future<void> deleteAllByContactId(int contactId) async {
await (delete(signalContactPreKeys) await (delete(signalContactPreKeys)
..where((t) => t.contactId.equals(contactId))) ..where((t) => t.contactId.equals(contactId)))
.go(); .go();
@ -24,7 +24,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
.go(); .go();
} }
Future deleteAllPreKeysByContactId(int contactId) async { Future<void> deleteAllPreKeysByContactId(int contactId) async {
await (delete(signalContactPreKeys) await (delete(signalContactPreKeys)
..where((t) => t.contactId.equals(contactId))) ..where((t) => t.contactId.equals(contactId)))
.go(); .go();

View file

@ -1,7 +1,7 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
class ConnectIdentityKeyStore extends IdentityKeyStore { class ConnectIdentityKeyStore extends IdentityKeyStore {
@ -12,13 +12,11 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
@override @override
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async { Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
SignalIdentityKeyStore? identity = final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
..where((t) => ..where((t) =>
t.deviceId.equals(address.getDeviceId()) & t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()))) t.name.equals(address.getName())))
.getSingleOrNull(); .getSingleOrNull();
if (identity == null) return null; if (identity == null) return null;
return IdentityKey.fromBytes(identity.identityKey, 0); return IdentityKey.fromBytes(identity.identityKey, 0);
} }
@ -37,7 +35,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
return false; return false;
} }
return trusted == null || return trusted == null ||
ListEquality().equals(trusted.serialize(), identityKey.serialize()); const ListEquality<dynamic>()
.equals(trusted.serialize(), identityKey.serialize());
} }
@override @override

View file

@ -1,6 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -21,7 +21,7 @@ class ConnectPreKeyStore extends PreKeyStore {
if (preKeyRecord.isEmpty) { if (preKeyRecord.isEmpty) {
throw InvalidKeyIdException('No such preKey record! - $preKeyId'); throw InvalidKeyIdException('No such preKey record! - $preKeyId');
} }
Uint8List preKey = preKeyRecord.first.preKey; final preKey = preKeyRecord.first.preKey;
return PreKeyRecord.fromBuffer(preKey); return PreKeyRecord.fromBuffer(preKey);
} }
@ -42,7 +42,7 @@ class ConnectPreKeyStore extends PreKeyStore {
try { try {
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
} catch (e) { } catch (e) {
Log.error("$e"); Log.error('$e');
} }
} }
} }

View file

@ -7,24 +7,24 @@ import 'package:twonly/src/constants/secure_storage_keys.dart';
class ConnectSignedPreKeyStore extends SignedPreKeyStore { class ConnectSignedPreKeyStore extends SignedPreKeyStore {
Future<HashMap<int, Uint8List>> getStore() async { Future<HashMap<int, Uint8List>> getStore() async {
final storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final storeSerialized = await storage.read( final storeSerialized = await storage.read(
key: SecureStorageKeys.signalSignedPreKey, key: SecureStorageKeys.signalSignedPreKey,
); );
var store = HashMap<int, Uint8List>(); final store = HashMap<int, Uint8List>();
if (storeSerialized == null) { if (storeSerialized == null) {
return store; return store;
} }
final storeHashMap = json.decode(storeSerialized); final storeHashMap = json.decode(storeSerialized) as List<List<dynamic>>;
for (final item in storeHashMap) { for (final item in storeHashMap) {
store[item[0]] = base64Decode(item[1]); store[item[0] as int] = base64Decode(item[1] as String);
} }
return store; return store;
} }
Future safeStore(HashMap<int, Uint8List> store) async { Future<void> safeStore(HashMap<int, Uint8List> store) async {
final storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
var storeHashMap = []; final storeHashMap = <List<dynamic>>[];
for (final item in store.entries) { for (final item in store.entries) {
storeHashMap.add([item.key, base64Encode(item.value)]); storeHashMap.add([item.key, base64Encode(item.value)]);
} }

View file

@ -6,7 +6,6 @@ enum UploadState {
readyToUpload, readyToUpload,
uploadTaskStarted, uploadTaskStarted,
receiverNotified, receiverNotified,
// after all users notified all media files that are not storable by the other person will be deleted
} }
@DataClassName('MediaUpload') @DataClassName('MediaUpload')
@ -16,18 +15,30 @@ class MediaUploads extends Table {
textEnum<UploadState>().withDefault(Constant(UploadState.pending.name))(); textEnum<UploadState>().withDefault(Constant(UploadState.pending.name))();
TextColumn get metadata => TextColumn get metadata =>
text().map(MediaUploadMetadataConverter()).nullable()(); text().map(const MediaUploadMetadataConverter()).nullable()();
/// exists in UploadState.addedToMessagesDb /// exists in UploadState.addedToMessagesDb
TextColumn get messageIds => text().map(IntListTypeConverter()).nullable()(); TextColumn get messageIds => text().map(IntListTypeConverter()).nullable()();
TextColumn get encryptionData => TextColumn get encryptionData =>
text().map(MediaEncryptionDataConverter()).nullable()(); text().map(const MediaEncryptionDataConverter()).nullable()();
} }
// --- state ---- // --- state ----
class MediaUploadMetadata { class MediaUploadMetadata {
MediaUploadMetadata();
factory MediaUploadMetadata.fromJson(Map<String, dynamic> json) {
return MediaUploadMetadata()
..contactIds = List<int>.from(json['contactIds'] as Iterable<dynamic>)
..isRealTwonly = json['isRealTwonly'] as bool
..isVideo = json['isVideo'] as bool
..mirrorVideo = json['mirrorVideo'] as bool
..maxShowTime = json['maxShowTime'] as int
..maxShowTime = json['maxShowTime'] as int
..messageSendAt = DateTime.parse(json['messageSendAt'] as String);
}
late List<int> contactIds; late List<int> contactIds;
late bool isRealTwonly; late bool isRealTwonly;
late int maxShowTime; late int maxShowTime;
@ -35,8 +46,6 @@ class MediaUploadMetadata {
late bool isVideo; late bool isVideo;
late bool mirrorVideo; late bool mirrorVideo;
MediaUploadMetadata();
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'contactIds': contactIds, 'contactIds': contactIds,
@ -47,28 +56,26 @@ class MediaUploadMetadata {
'messageSendAt': messageSendAt.toIso8601String(), 'messageSendAt': messageSendAt.toIso8601String(),
}; };
} }
factory MediaUploadMetadata.fromJson(Map<String, dynamic> json) {
MediaUploadMetadata state = MediaUploadMetadata();
state.contactIds = List<int>.from(json['contactIds']);
state.isRealTwonly = json['isRealTwonly'];
state.isVideo = json['isVideo'];
state.mirrorVideo = json['mirrorVideo'];
state.maxShowTime = json['maxShowTime'];
state.maxShowTime = json['maxShowTime'];
state.messageSendAt = DateTime.parse(json['messageSendAt']);
return state;
}
} }
class MediaEncryptionData { class MediaEncryptionData {
MediaEncryptionData();
factory MediaEncryptionData.fromJson(Map<String, dynamic> json) {
return MediaEncryptionData()
..sha2Hash = List<int>.from(json['sha2Hash'] as Iterable<dynamic>)
..encryptionKey =
List<int>.from(json['encryptionKey'] as Iterable<dynamic>)
..encryptionMac =
List<int>.from(json['encryptionMac'] as Iterable<dynamic>)
..encryptionNonce =
List<int>.from(json['encryptionNonce'] as Iterable<dynamic>);
}
late List<int> sha2Hash; late List<int> sha2Hash;
late List<int> encryptionKey; late List<int> encryptionKey;
late List<int> encryptionMac; late List<int> encryptionMac;
late List<int> encryptionNonce; late List<int> encryptionNonce;
MediaEncryptionData();
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'sha2Hash': sha2Hash, 'sha2Hash': sha2Hash,
@ -77,15 +84,6 @@ class MediaEncryptionData {
'encryptionNonce': encryptionNonce, 'encryptionNonce': encryptionNonce,
}; };
} }
factory MediaEncryptionData.fromJson(Map<String, dynamic> json) {
MediaEncryptionData state = MediaEncryptionData();
state.sha2Hash = List<int>.from(json['sha2Hash']);
state.encryptionKey = List<int>.from(json['encryptionKey']);
state.encryptionMac = List<int>.from(json['encryptionMac']);
state.encryptionNonce = List<int>.from(json['encryptionNonce']);
return state;
}
} }
// --- converters ---- // --- converters ----
@ -93,7 +91,7 @@ class MediaEncryptionData {
class IntListTypeConverter extends TypeConverter<List<int>, String> { class IntListTypeConverter extends TypeConverter<List<int>, String> {
@override @override
List<int> fromSql(String fromDb) { List<int> fromSql(String fromDb) {
return List<int>.from(jsonDecode(fromDb)); return List<int>.from(jsonDecode(fromDb) as Iterable<dynamic>);
} }
@override @override

View file

@ -1,6 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart' import 'package:drift_flutter/drift_flutter.dart'
show driftDatabase, DriftNativeOptions; show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/daos/media_downloads_dao.dart'; import 'package:twonly/src/database/daos/media_downloads_dao.dart';
@ -73,15 +73,15 @@ class TwonlyDatabase extends _$TwonlyDatabase {
}, },
onUpgrade: stepByStep( onUpgrade: stepByStep(
from1To2: (m, schema) async { from1To2: (m, schema) async {
m.addColumn(schema.messages, schema.messages.errorWhileSending); await m.addColumn(schema.messages, schema.messages.errorWhileSending);
}, },
from2To3: (m, schema) async { from2To3: (m, schema) async {
m.addColumn(schema.contacts, schema.contacts.archived); await m.addColumn(schema.contacts, schema.contacts.archived);
m.addColumn( await m.addColumn(
schema.contacts, schema.contacts.deleteMessagesAfterXMinutes); schema.contacts, schema.contacts.deleteMessagesAfterXMinutes);
}, },
from3To4: (m, schema) async { from3To4: (m, schema) async {
m.createTable(schema.mediaUploads); await m.createTable(schema.mediaUploads);
await m.alterTable(TableMigration( await m.alterTable(TableMigration(
schema.mediaUploads, schema.mediaUploads,
columnTransformer: { columnTransformer: {
@ -91,19 +91,19 @@ class TwonlyDatabase extends _$TwonlyDatabase {
)); ));
}, },
from4To5: (m, schema) async { from4To5: (m, schema) async {
m.createTable(mediaDownloads); await m.createTable(mediaDownloads);
m.addColumn(schema.messages, schema.messages.mediaDownloadId); await m.addColumn(schema.messages, schema.messages.mediaDownloadId);
m.addColumn(schema.messages, schema.messages.mediaUploadId); await m.addColumn(schema.messages, schema.messages.mediaUploadId);
}, },
from5To6: (m, schema) async { from5To6: (m, schema) async {
m.addColumn(schema.messages, schema.messages.mediaStored); await m.addColumn(schema.messages, schema.messages.mediaStored);
}, },
from6To7: (m, schema) async { from6To7: (m, schema) async {
m.addColumn(schema.contacts, schema.contacts.pinned); await m.addColumn(schema.contacts, schema.contacts.pinned);
}, },
from7To8: (m, schema) async { from7To8: (m, schema) async {
m.addColumn(schema.contacts, schema.contacts.alsoBestFriend); await m.addColumn(schema.contacts, schema.contacts.alsoBestFriend);
m.addColumn(schema.contacts, schema.contacts.lastFlameSync); await m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
}, },
from8To9: (m, schema) async { from8To9: (m, schema) async {
await m.alterTable(TableMigration( await m.alterTable(TableMigration(
@ -115,29 +115,29 @@ class TwonlyDatabase extends _$TwonlyDatabase {
)); ));
}, },
from9To10: (m, schema) async { from9To10: (m, schema) async {
m.createTable(schema.signalContactPreKeys); await m.createTable(schema.signalContactPreKeys);
m.createTable(schema.signalContactSignedPreKeys); await m.createTable(schema.signalContactSignedPreKeys);
m.addColumn(schema.contacts, schema.contacts.deleted); await m.addColumn(schema.contacts, schema.contacts.deleted);
}, },
from10To11: (m, schema) async { from10To11: (m, schema) async {
m.createTable(schema.messageRetransmissions); await m.createTable(schema.messageRetransmissions);
}, },
from11To12: (m, schema) async { from11To12: (m, schema) async {
m.addColumn(schema.messageRetransmissions, await m.addColumn(schema.messageRetransmissions,
schema.messageRetransmissions.willNotGetACKByUser); schema.messageRetransmissions.willNotGetACKByUser);
}, },
from12To13: (m, schema) async { from12To13: (m, schema) async {
m.dropColumn( await m.dropColumn(
schema.messageRetransmissions, "will_not_get_a_c_k_by_user"); schema.messageRetransmissions, 'will_not_get_a_c_k_by_user');
}, },
from13To14: (m, schema) async { from13To14: (m, schema) async {
m.addColumn(schema.messageRetransmissions, await m.addColumn(schema.messageRetransmissions,
schema.messageRetransmissions.encryptedHash); schema.messageRetransmissions.encryptedHash);
}, },
from14To15: (m, schema) async { from14To15: (m, schema) async {
m.dropColumn(schema.mediaUploads, "upload_tokens"); await m.dropColumn(schema.mediaUploads, 'upload_tokens');
m.dropColumn(schema.mediaUploads, "already_notified"); await m.dropColumn(schema.mediaUploads, 'already_notified');
m.addColumn( await m.addColumn(
schema.messages, schema.messages.mediaRetransmissionState); schema.messages, schema.messages.mediaRetransmissionState);
}, },
), ),
@ -161,13 +161,13 @@ class TwonlyDatabase extends _$TwonlyDatabase {
} }
} }
Future deleteDataForTwonlySafe() async { Future<void> deleteDataForTwonlySafe() async {
await delete(messages).go(); await delete(messages).go();
await delete(messageRetransmissions).go(); await delete(messageRetransmissions).go();
await delete(mediaDownloads).go(); await delete(mediaDownloads).go();
await delete(mediaUploads).go(); await delete(mediaUploads).go();
await update(contacts).write( await update(contacts).write(
ContactsCompanion( const ContactsCompanion(
avatarSvg: Value(null), avatarSvg: Value(null),
myAvatarCounter: Value(0), myAvatarCounter: Value(0),
), ),
@ -177,7 +177,7 @@ class TwonlyDatabase extends _$TwonlyDatabase {
await (delete(signalPreKeyStores) await (delete(signalPreKeyStores)
..where((t) => (t.createdAt.isSmallerThanValue( ..where((t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract( DateTime.now().subtract(
Duration(days: 25), const Duration(days: 25),
), ),
)))) ))))
.go(); .go();

View file

@ -89,7 +89,7 @@
"messageSendState_Sending": "Wird gesendet", "messageSendState_Sending": "Wird gesendet",
"messageSendState_TapToLoad": "Tippe zum Laden", "messageSendState_TapToLoad": "Tippe zum Laden",
"messageSendState_Loading": "Herunterladen", "messageSendState_Loading": "Herunterladen",
"messageStoredInGalery": "Gespeichert", "messageStoredInGallery": "Gespeichert",
"messageReopened": "Erneut geöffnet", "messageReopened": "Erneut geöffnet",
"@messageReopened": {}, "@messageReopened": {},
"imageEditorDrawOk": "Zeichnung machen", "imageEditorDrawOk": "Zeichnung machen",
@ -329,7 +329,7 @@
"appOutdatedBtn": "Jetzt aktualisieren.", "appOutdatedBtn": "Jetzt aktualisieren.",
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.", "doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
"retransmissionRequested": "Wird erneut versucht.", "retransmissionRequested": "Wird erneut versucht.",
"testPaymentMethode": "twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!", "testPaymentMethod": "twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!",
"testingAccountTitle": "Tester-Zugang", "testingAccountTitle": "Tester-Zugang",
"testingAccountBody": "Danke für dein Interesse! Wir werden deine Anfrage prüfen und den Plan so schnell wie möglich aktivieren. Da wir uns jedoch noch in einer Testphase befinden, ist die Anzahl der Tester-Konten begrenzt. Wir werden dich jedoch benachrichtigen, sobald dir ein Platz zugewiesen wurde." "testingAccountBody": "Danke für dein Interesse! Wir werden deine Anfrage prüfen und den Plan so schnell wie möglich aktivieren. Da wir uns jedoch noch in einer Testphase befinden, ist die Anzahl der Tester-Konten begrenzt. Wir werden dich jedoch benachrichtigen, sobald dir ein Platz zugewiesen wurde."
} }

View file

@ -145,8 +145,8 @@
"@messageSendState_TapToLoad": {}, "@messageSendState_TapToLoad": {},
"messageSendState_Loading": "Downloading", "messageSendState_Loading": "Downloading",
"@messageSendState_Loading": {}, "@messageSendState_Loading": {},
"messageStoredInGalery": "Stored in gallery", "messageStoredInGallery": "Stored in gallery",
"@messageStoredInGalery": {}, "@messageStoredInGallery": {},
"messageReopened": "Re-opened", "messageReopened": "Re-opened",
"@messageReopened": {}, "@messageReopened": {},
"imageEditorDrawOk": "Take drawing", "imageEditorDrawOk": "Take drawing",
@ -486,7 +486,7 @@
"appOutdatedBtn": "Update Now", "appOutdatedBtn": "Update Now",
"doubleClickToReopen": "Double-click\nto open again", "doubleClickToReopen": "Double-click\nto open again",
"retransmissionRequested": "Retransmission requested", "retransmissionRequested": "Retransmission requested",
"testPaymentMethode": "twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!", "testPaymentMethod": "twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!",
"testingAccountTitle": "Tester account activation", "testingAccountTitle": "Tester account activation",
"testingAccountBody": "Thank you for your interest! We will check your request and activate the plan as soon as possible. However, as we are currently still in a test phase, the number of tester accounts is limited. However, we will notify you as soon as you have been allocated a place." "testingAccountBody": "Thank you for your interest! We will check your request and activate the plan as soon as possible. However, as we are currently still in a test phase, the number of tester accounts is limited. However, we will notify you as soon as you have been allocated a place."
} }

View file

@ -542,11 +542,11 @@ abstract class AppLocalizations {
/// **'Downloading'** /// **'Downloading'**
String get messageSendState_Loading; String get messageSendState_Loading;
/// No description provided for @messageStoredInGalery. /// No description provided for @messageStoredInGallery.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Stored in gallery'** /// **'Stored in gallery'**
String get messageStoredInGalery; String get messageStoredInGallery;
/// No description provided for @messageReopened. /// No description provided for @messageReopened.
/// ///
@ -2012,11 +2012,11 @@ abstract class AppLocalizations {
/// **'Retransmission requested'** /// **'Retransmission requested'**
String get retransmissionRequested; String get retransmissionRequested;
/// No description provided for @testPaymentMethode. /// No description provided for @testPaymentMethod.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!'** /// **'twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!'**
String get testPaymentMethode; String get testPaymentMethod;
/// No description provided for @testingAccountTitle. /// No description provided for @testingAccountTitle.
/// ///

View file

@ -253,7 +253,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get messageSendState_Loading => 'Herunterladen'; String get messageSendState_Loading => 'Herunterladen';
@override @override
String get messageStoredInGalery => 'Gespeichert'; String get messageStoredInGallery => 'Gespeichert';
@override @override
String get messageReopened => 'Erneut geöffnet'; String get messageReopened => 'Erneut geöffnet';
@ -1068,7 +1068,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get retransmissionRequested => 'Wird erneut versucht.'; String get retransmissionRequested => 'Wird erneut versucht.';
@override @override
String get testPaymentMethode => String get testPaymentMethod =>
'twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!'; 'twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!';
@override @override

View file

@ -250,7 +250,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get messageSendState_Loading => 'Downloading'; String get messageSendState_Loading => 'Downloading';
@override @override
String get messageStoredInGalery => 'Stored in gallery'; String get messageStoredInGallery => 'Stored in gallery';
@override @override
String get messageReopened => 'Re-opened'; String get messageReopened => 'Re-opened';
@ -1062,7 +1062,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get retransmissionRequested => 'Retransmission requested'; String get retransmissionRequested => 'Retransmission requested';
@override @override
String get testPaymentMethode => String get testPaymentMethod =>
'twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!'; 'twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!';
@override @override

View file

@ -1,3 +1,5 @@
// ignore_for_file: strict_raw_type, prefer_constructors_over_static_methods
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -34,6 +36,14 @@ extension MessageKindExtension on MessageKind {
} }
class MessageJson { class MessageJson {
MessageJson({
required this.kind,
required this.content,
required this.timestamp,
this.messageReceiverId,
this.messageSenderId,
this.retransId,
});
final MessageKind kind; final MessageKind kind;
final MessageContent? content; final MessageContent? content;
final int? messageReceiverId; final int? messageReceiverId;
@ -41,22 +51,13 @@ class MessageJson {
int? retransId; int? retransId;
DateTime timestamp; DateTime timestamp;
MessageJson({
required this.kind,
this.messageReceiverId,
this.messageSenderId,
this.retransId,
required this.content,
required this.timestamp,
});
@override @override
String toString() { String toString() {
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)'; return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
} }
static MessageJson fromJson(Map<String, dynamic> json) { static MessageJson fromJson(Map<String, dynamic> json) {
final kind = MessageKindExtension.fromString(json["kind"]); final kind = MessageKindExtension.fromString(json['kind'] as String);
return MessageJson( return MessageJson(
kind: kind, kind: kind,
@ -65,7 +66,7 @@ class MessageJson {
retransId: (json['retransId'] as num?)?.toInt(), retransId: (json['retransId'] as num?)?.toInt(),
content: MessageContent.fromJson( content: MessageContent.fromJson(
kind, json['content'] as Map<String, dynamic>), kind, json['content'] as Map<String, dynamic>),
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp']), timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
); );
} }
@ -100,9 +101,15 @@ class MessageContent {
return AckContent.fromJson(json); return AckContent.fromJson(json);
case MessageKind.signalDecryptError: case MessageKind.signalDecryptError:
return SignalDecryptErrorContent.fromJson(json); return SignalDecryptErrorContent.fromJson(json);
default: case MessageKind.storedMediaFile:
return null; case MessageKind.contactRequest:
case MessageKind.rejectRequest:
case MessageKind.acceptRequest:
case MessageKind.opened:
case MessageKind.requestPushKey:
case MessageKind.receiveMediaError:
} }
return null;
} }
Map toJson() { Map toJson() {
@ -111,15 +118,6 @@ class MessageContent {
} }
class MediaMessageContent extends MessageContent { class MediaMessageContent extends MessageContent {
final int maxShowTime;
final bool isRealTwonly;
final bool isVideo;
final bool mirrorVideo;
final List<int>? downloadToken;
final List<int>? encryptionKey;
final List<int>? encryptionMac;
final List<int>? encryptionNonce;
MediaMessageContent({ MediaMessageContent({
required this.maxShowTime, required this.maxShowTime,
required this.isRealTwonly, required this.isRealTwonly,
@ -130,25 +128,33 @@ class MediaMessageContent extends MessageContent {
this.encryptionMac, this.encryptionMac,
this.encryptionNonce, this.encryptionNonce,
}); });
final int maxShowTime;
final bool isRealTwonly;
final bool isVideo;
final bool mirrorVideo;
final List<int>? downloadToken;
final List<int>? encryptionKey;
final List<int>? encryptionMac;
final List<int>? encryptionNonce;
static MediaMessageContent fromJson(Map json) { static MediaMessageContent fromJson(Map json) {
return MediaMessageContent( return MediaMessageContent(
downloadToken: json['downloadToken'] == null downloadToken: json['downloadToken'] == null
? null ? null
: List<int>.from(json['downloadToken']), : List<int>.from(json['downloadToken'] as List),
encryptionKey: json['encryptionKey'] == null encryptionKey: json['encryptionKey'] == null
? null ? null
: List<int>.from(json['encryptionKey']), : List<int>.from(json['encryptionKey'] as List),
encryptionMac: json['encryptionMac'] == null encryptionMac: json['encryptionMac'] == null
? null ? null
: List<int>.from(json['encryptionMac']), : List<int>.from(json['encryptionMac'] as List),
encryptionNonce: json['encryptionNonce'] == null encryptionNonce: json['encryptionNonce'] == null
? null ? null
: List<int>.from(json['encryptionNonce']), : List<int>.from(json['encryptionNonce'] as List),
maxShowTime: json['maxShowTime'], maxShowTime: json['maxShowTime'] as int,
isRealTwonly: json['isRealTwonly'], isRealTwonly: json['isRealTwonly'] as bool,
isVideo: json['isVideo'] ?? false, isVideo: json['isVideo'] as bool? ?? false,
mirrorVideo: json['mirrorVideo'] ?? false, mirrorVideo: json['mirrorVideo'] as bool? ?? false,
); );
} }
@ -168,23 +174,23 @@ class MediaMessageContent extends MessageContent {
} }
class TextMessageContent extends MessageContent { class TextMessageContent extends MessageContent {
String text;
int? responseToMessageId;
int? responseToOtherMessageId;
TextMessageContent({ TextMessageContent({
required this.text, required this.text,
this.responseToMessageId, this.responseToMessageId,
this.responseToOtherMessageId, this.responseToOtherMessageId,
}); });
String text;
int? responseToMessageId;
int? responseToOtherMessageId;
static TextMessageContent fromJson(Map json) { static TextMessageContent fromJson(Map json) {
return TextMessageContent( return TextMessageContent(
text: json['text'], text: json['text'] as String,
responseToOtherMessageId: json.containsKey('responseToOtherMessageId') responseToOtherMessageId: json.containsKey('responseToOtherMessageId')
? json['responseToOtherMessageId'] ? json['responseToOtherMessageId'] as int?
: null, : null,
responseToMessageId: json.containsKey('responseToMessageId') responseToMessageId: json.containsKey('responseToMessageId')
? json['responseToMessageId'] ? json['responseToMessageId'] as int?
: null); : null);
} }
@ -199,11 +205,11 @@ class TextMessageContent extends MessageContent {
} }
class ReopenedMediaFileContent extends MessageContent { class ReopenedMediaFileContent extends MessageContent {
int messageId;
ReopenedMediaFileContent({required this.messageId}); ReopenedMediaFileContent({required this.messageId});
int messageId;
static ReopenedMediaFileContent fromJson(Map json) { static ReopenedMediaFileContent fromJson(Map json) {
return ReopenedMediaFileContent(messageId: json['messageId']); return ReopenedMediaFileContent(messageId: json['messageId'] as int);
} }
@override @override
@ -213,12 +219,12 @@ class ReopenedMediaFileContent extends MessageContent {
} }
class SignalDecryptErrorContent extends MessageContent { class SignalDecryptErrorContent extends MessageContent {
List<int> encryptedHash;
SignalDecryptErrorContent({required this.encryptedHash}); SignalDecryptErrorContent({required this.encryptedHash});
List<int> encryptedHash;
static SignalDecryptErrorContent fromJson(Map json) { static SignalDecryptErrorContent fromJson(Map json) {
return SignalDecryptErrorContent( return SignalDecryptErrorContent(
encryptedHash: List<int>.from(json['encryptedHash']), encryptedHash: List<int>.from(json['encryptedHash'] as List),
); );
} }
@ -231,14 +237,14 @@ class SignalDecryptErrorContent extends MessageContent {
} }
class AckContent extends MessageContent { class AckContent extends MessageContent {
AckContent({required this.messageIdToAck, required this.retransIdToAck});
int? messageIdToAck; int? messageIdToAck;
int retransIdToAck; int retransIdToAck;
AckContent({required this.messageIdToAck, required this.retransIdToAck});
static AckContent fromJson(Map json) { static AckContent fromJson(Map json) {
return AckContent( return AckContent(
messageIdToAck: json['messageIdToAck'], messageIdToAck: json['messageIdToAck'] as int,
retransIdToAck: json['retransIdToAck'], retransIdToAck: json['retransIdToAck'] as int,
); );
} }
@ -252,14 +258,14 @@ class AckContent extends MessageContent {
} }
class ProfileContent extends MessageContent { class ProfileContent extends MessageContent {
ProfileContent({required this.avatarSvg, required this.displayName});
String avatarSvg; String avatarSvg;
String displayName; String displayName;
ProfileContent({required this.avatarSvg, required this.displayName});
static ProfileContent fromJson(Map json) { static ProfileContent fromJson(Map json) {
return ProfileContent( return ProfileContent(
avatarSvg: json['avatarSvg'], avatarSvg: json['avatarSvg'] as String,
displayName: json['displayName'], displayName: json['displayName'] as String,
); );
} }
@ -270,14 +276,14 @@ class ProfileContent extends MessageContent {
} }
class PushKeyContent extends MessageContent { class PushKeyContent extends MessageContent {
PushKeyContent({required this.keyId, required this.key});
int keyId; int keyId;
List<int> key; List<int> key;
PushKeyContent({required this.keyId, required this.key});
static PushKeyContent fromJson(Map json) { static PushKeyContent fromJson(Map json) {
return PushKeyContent( return PushKeyContent(
keyId: json['keyId'], keyId: json['keyId'] as int,
key: List<int>.from(json['key']), key: List<int>.from(json['key'] as List),
); );
} }
@ -291,21 +297,20 @@ class PushKeyContent extends MessageContent {
} }
class FlameSyncContent extends MessageContent { class FlameSyncContent extends MessageContent {
int flameCounter;
DateTime lastFlameCounterChange;
bool bestFriend;
FlameSyncContent( FlameSyncContent(
{required this.flameCounter, {required this.flameCounter,
required this.bestFriend, required this.bestFriend,
required this.lastFlameCounterChange}); required this.lastFlameCounterChange});
int flameCounter;
DateTime lastFlameCounterChange;
bool bestFriend;
static FlameSyncContent fromJson(Map json) { static FlameSyncContent fromJson(Map json) {
return FlameSyncContent( return FlameSyncContent(
flameCounter: json['flameCounter'], flameCounter: json['flameCounter'] as int,
bestFriend: json['bestFriend'], bestFriend: json['bestFriend'] as bool,
lastFlameCounterChange: lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch(
DateTime.fromMillisecondsSinceEpoch(json['lastFlameCounterChange']), json['lastFlameCounterChange'] as int),
); );
} }

View file

@ -1,10 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/media_upload.dart' as send;
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/media_upload.dart' as send;
import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/services/thumbnail.service.dart';
class MemoryItem { class MemoryItem {
@ -28,45 +29,45 @@ class MemoryItem {
static Future<Map<int, MemoryItem>> convertFromMessages( static Future<Map<int, MemoryItem>> convertFromMessages(
List<Message> messages, List<Message> messages,
) async { ) async {
Map<int, MemoryItem> items = {}; final items = <int, MemoryItem>{};
for (final message in messages) { for (final message in messages) {
bool isSend = message.messageOtherId == null; final isSend = message.messageOtherId == null;
int id = message.mediaUploadId ?? message.messageId; final id = message.mediaUploadId ?? message.messageId;
final basePath = await send.getMediaFilePath( final basePath = await send.getMediaFilePath(
isSend ? message.mediaUploadId! : message.messageId, isSend ? message.mediaUploadId! : message.messageId,
isSend ? "send" : "received", isSend ? 'send' : 'received',
); );
File? imagePath; File? imagePath;
late File thumbnailFile; late File thumbnailFile;
File? videoPath; File? videoPath;
if (await File("$basePath.mp4").exists()) { if (File('$basePath.mp4').existsSync()) {
videoPath = File("$basePath.mp4"); videoPath = File('$basePath.mp4');
thumbnailFile = getThumbnailPath(videoPath); thumbnailFile = getThumbnailPath(videoPath);
if (!await thumbnailFile.exists()) { if (!thumbnailFile.existsSync()) {
await createThumbnailsForVideo(videoPath); await createThumbnailsForVideo(videoPath);
} }
} else if (await File("$basePath.png").exists()) { } else if (File('$basePath.png').existsSync()) {
imagePath = File("$basePath.png"); imagePath = File('$basePath.png');
thumbnailFile = getThumbnailPath(imagePath); thumbnailFile = getThumbnailPath(imagePath);
if (!await thumbnailFile.exists()) { if (!thumbnailFile.existsSync()) {
await createThumbnailsForImage(imagePath); await createThumbnailsForImage(imagePath);
} }
} else { } else {
if (message.mediaStored) { if (message.mediaStored) {
/// media file was deleted, ... remove the file /// media file was deleted, ... remove the file
twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId, message.messageId,
MessagesCompanion( const MessagesCompanion(
mediaStored: Value(false), mediaStored: Value(false),
), ),
); );
} }
continue; continue;
} }
bool mirrorVideo = false; var mirrorVideo = false;
if (videoPath != null) { if (videoPath != null) {
MediaMessageContent content = final content = MediaMessageContent.fromJson(
MediaMessageContent.fromJson(jsonDecode(message.contentJson!)); jsonDecode(message.contentJson!) as Map);
mirrorVideo = content.mirrorVideo; mirrorVideo = content.mirrorVideo;
} }

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,18 @@
// ignore_for_file: avoid_dynamic_calls, strict_raw_type
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.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';
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -18,24 +23,22 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server; as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/server_messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/io.dart';
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
final lockConnecting = Mutex(); final lockConnecting = Mutex();
@ -45,12 +48,12 @@ final lockRetransStore = Mutex();
/// It handles errors and does automatically tries to reconnect on /// It handles errors and does automatically tries to reconnect on
/// errors or network changes. /// errors or network changes.
class ApiService { class ApiService {
final String apiHost = (kDebugMode) ? "10.99.0.140:3030" : "api.twonly.eu"; ApiService();
final String apiSecure = (kDebugMode) ? "" : "s"; final String apiHost = kDebugMode ? '10.99.0.140:3030' : 'api.twonly.eu';
final String apiSecure = kDebugMode ? '' : 's';
bool appIsOutdated = false; bool appIsOutdated = false;
bool isAuthenticated = false; bool isAuthenticated = false;
ApiService();
// reconnection params // reconnection params
Timer? reconnectionTimer; Timer? reconnectionTimer;
@ -69,51 +72,51 @@ class ApiService {
_channel = channel; _channel = channel;
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError); _channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
await _channel!.ready; await _channel!.ready;
Log.info("websocket connected to $apiUrl"); Log.info('websocket connected to $apiUrl');
return true; return true;
} on WebSocketChannelException catch (e) { } on WebSocketChannelException catch (e) {
if (!e.message if (!e.message
.toString() .toString()
.contains("No address associated with hostname")) { .contains('No address associated with hostname')) {
Log.error("could not connect to api got: $e"); Log.error('could not connect to api got: $e');
} }
return false; return false;
} }
} }
// Function is called after the user is authenticated at the server // Function is called after the user is authenticated at the server
Future onAuthenticated() async { Future<void> onAuthenticated() async {
isAuthenticated = true; isAuthenticated = true;
initFCMAfterAuthenticated(); await initFCMAfterAuthenticated();
globalCallbackConnectionState(true); globalCallbackConnectionState(isConnected: true);
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {
retransmitRawBytes(); unawaited(retransmitRawBytes());
tryTransmitMessages(); unawaited(tryTransmitMessages());
retryMediaUpload(false); unawaited(retryMediaUpload(false));
tryDownloadAllMediaFiles(); unawaited(tryDownloadAllMediaFiles());
notifyContactsAboutProfileChange(); unawaited(notifyContactsAboutProfileChange());
twonlyDB.markUpdated(); twonlyDB.markUpdated();
syncFlameCounters(); unawaited(syncFlameCounters());
setupNotificationWithUsers(); unawaited(setupNotificationWithUsers());
signalHandleNewServerConnection(); unawaited(signalHandleNewServerConnection());
} }
} }
Future onConnected() async { Future<void> onConnected() async {
await authenticate(); await authenticate();
_reconnectionDelay = 5; _reconnectionDelay = 5;
globalCallbackConnectionState(true); globalCallbackConnectionState(isConnected: true);
} }
Future onClosed() async { Future<void> onClosed() async {
_channel = null; _channel = null;
isAuthenticated = false; isAuthenticated = false;
globalCallbackConnectionState(false); globalCallbackConnectionState(isConnected: false);
await twonlyDB.messagesDao.resetPendingDownloadState(); await twonlyDB.messagesDao.resetPendingDownloadState();
} }
Future startReconnectionTimer() async { Future<void> startReconnectionTimer() async {
reconnectionTimer?.cancel(); reconnectionTimer?.cancel();
reconnectionTimer ??= Timer(Duration(seconds: _reconnectionDelay), () { reconnectionTimer ??= Timer(Duration(seconds: _reconnectionDelay), () {
reconnectionTimer = null; reconnectionTimer = null;
@ -122,18 +125,18 @@ class ApiService {
_reconnectionDelay += 5; _reconnectionDelay += 5;
} }
Future close(Function callback) async { Future<void> close(Function callback) async {
Log.info("closing websocket connection"); Log.info('closing websocket connection');
if (_channel != null) { if (_channel != null) {
await _channel!.sink.close(); await _channel!.sink.close();
onClosed(); await onClosed();
callback(); callback();
return; return;
} }
callback(); callback();
} }
Future listenToNetworkChanges() async { Future<void> listenToNetworkChanges() async {
if (connectivitySubscription != null) { if (connectivitySubscription != null) {
return; return;
} }
@ -155,7 +158,7 @@ class ApiService {
reconnectionTimer = null; reconnectionTimer = null;
final user = await getUser(); final user = await getUser();
if (user != null && user.isDemoUser) { if (user != null && user.isDemoUser) {
globalCallbackConnectionState(true); globalCallbackConnectionState(isConnected: true);
return false; return false;
} }
return lockConnecting.protect<bool>(() async { return lockConnecting.protect<bool>(() async {
@ -168,9 +171,9 @@ class ApiService {
isAuthenticated = false; isAuthenticated = false;
String apiUrl = "ws$apiSecure://$apiHost/api/client"; final apiUrl = 'ws$apiSecure://$apiHost/api/client';
Log.info("connecting to $apiUrl"); Log.info('connecting to $apiUrl');
if (await _connectTo(apiUrl)) { if (await _connectTo(apiUrl)) {
await onConnected(); await onConnected();
@ -183,18 +186,18 @@ class ApiService {
bool get isConnected => _channel != null && _channel!.closeCode != null; bool get isConnected => _channel != null && _channel!.closeCode != null;
void _onDone() { void _onDone() {
Log.info("websocket closed without error"); Log.info('websocket closed without error');
onClosed(); onClosed();
} }
void _onError(dynamic e) { void _onError(dynamic e) {
Log.error("websocket error: $e"); Log.error('websocket error: $e');
onClosed(); onClosed();
} }
void _onData(dynamic msgBuffer) async { void _onData(dynamic msgBuffer) async {
try { try {
final msg = server.ServerToClient.fromBuffer(msgBuffer); final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List);
if (msg.v0.hasResponse()) { if (msg.v0.hasResponse()) {
removeFromRetransmissionBuffer(msg.v0.seq); removeFromRetransmissionBuffer(msg.v0.seq);
messagesV0[msg.v0.seq] = msg; messagesV0[msg.v0.seq] = msg;
@ -202,7 +205,7 @@ class ApiService {
await handleServerMessage(msg); await handleServerMessage(msg);
} }
} catch (e) { } catch (e) {
Log.error("Error parsing the servers message: $e"); Log.error('Error parsing the servers message: $e');
} }
} }
@ -218,57 +221,57 @@ class ApiService {
return tmp; return tmp;
} }
if (DateTime.now().difference(startTime) > timeout) { if (DateTime.now().difference(startTime) > timeout) {
Log.error("Timeout for message $seq"); Log.error('Timeout for message $seq');
return null; return null;
} }
await Future.delayed(Duration(milliseconds: 10)); await Future.delayed(const Duration(milliseconds: 10));
} }
} }
Future sendResponse(ClientToServer response) async { Future<void> sendResponse(ClientToServer response) async {
if (_channel != null) { if (_channel != null) {
_channel!.sink.add(response.writeToBuffer()); _channel!.sink.add(response.writeToBuffer());
} }
} }
Future<Map<String, dynamic>> getRetransmission() async { Future<Map<String, dynamic>> getRetransmission() async {
return await KeyValueStore.get("rawbytes-to-retransmit") ?? {}; return (await KeyValueStore.get('rawbytes-to-retransmit')) ?? {};
} }
Future retransmitRawBytes() async { Future<void> retransmitRawBytes() async {
await lockRetransStore.protect(() async { await lockRetransStore.protect(() async {
var retransmit = await getRetransmission(); final retransmit = await getRetransmission();
if (retransmit.keys.isEmpty) return; if (retransmit.keys.isEmpty) return;
Log.info("retransmitting ${retransmit.keys.length} raw bytes messages"); Log.info('retransmitting ${retransmit.keys.length} raw bytes messages');
bool gotError = false; var gotError = false;
for (final seq in retransmit.keys) { for (final seq in retransmit.keys) {
try { try {
_channel!.sink.add(base64Decode(retransmit[seq])); _channel!.sink.add(base64Decode(retransmit[seq] as String));
} catch (e) { } catch (e) {
gotError = true; gotError = true;
Log.error("$e"); Log.error('$e');
} }
} }
if (!gotError) { if (!gotError) {
KeyValueStore.put("rawbytes-to-retransmit", {}); await KeyValueStore.put('rawbytes-to-retransmit', {});
} }
}); });
} }
Future addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async { Future<void> addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async {
await lockRetransStore.protect(() async { await lockRetransStore.protect(() async {
var retransmit = await getRetransmission(); final retransmit = await getRetransmission();
retransmit[seq.toString()] = base64Encode(bytes); retransmit[seq.toString()] = base64Encode(bytes);
KeyValueStore.put("rawbytes-to-retransmit", retransmit); await KeyValueStore.put('rawbytes-to-retransmit', retransmit);
}); });
} }
Future removeFromRetransmissionBuffer(Int64 seq) async { Future<void> removeFromRetransmissionBuffer(Int64 seq) async {
await lockRetransStore.protect(() async { await lockRetransStore.protect(() async {
var retransmit = await getRetransmission(); final retransmit = await getRetransmission();
if (retransmit.isEmpty) return; if (retransmit.isEmpty) return;
retransmit.remove(seq.toString()); retransmit.remove(seq.toString());
KeyValueStore.put("rawbytes-to-retransmit", retransmit); await KeyValueStore.put('rawbytes-to-retransmit', retransmit);
}); });
} }
@ -287,11 +290,11 @@ class ApiService {
final requestBytes = request.writeToBuffer(); final requestBytes = request.writeToBuffer();
if (ensureRetransmission) { if (ensureRetransmission) {
addToRetransmissionBuffer(seq, requestBytes); await addToRetransmissionBuffer(seq, requestBytes);
} }
if (_channel == null) { if (_channel == null) {
Log.warn("sending request while api is not connected"); Log.warn('sending request while api is not connected');
if (!await connect()) { if (!await connect()) {
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
} }
@ -302,12 +305,12 @@ class ApiService {
_channel!.sink.add(requestBytes); _channel!.sink.add(requestBytes);
Result res = asResult(await _waitForResponse(seq)); final res = asResult(await _waitForResponse(seq));
if (res.isError) { if (res.isError) {
Log.error("got error from server: ${res.error}"); Log.error('got error from server: ${res.error}');
if (res.error == ErrorCode.AppVersionOutdated) { if (res.error == ErrorCode.AppVersionOutdated) {
globalCallbackAppIsOutdated(); globalCallbackAppIsOutdated();
Log.error("App Version is OUTDATED."); Log.error('App Version is OUTDATED.');
appIsOutdated = true; appIsOutdated = true;
await close(() {}); await close(() {});
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
@ -320,16 +323,16 @@ class ApiService {
// this will send the request one more time. // this will send the request one more time.
return sendRequestSync(request, authenticated: false); return sendRequestSync(request, authenticated: false);
} else { } else {
Log.error("session is not authenticated"); Log.error('session is not authenticated');
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
} }
} }
} }
if (res.error == ErrorCode.UserIdNotFound && contactId != null) { if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
Log.error("Contact deleted their account $contactId."); Log.error('Contact deleted their account $contactId.');
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
contactId, contactId,
ContactsCompanion( const ContactsCompanion(
deleted: Value(true), deleted: Value(true),
), ),
); );
@ -339,8 +342,8 @@ class ApiService {
} }
Future<bool> tryAuthenticateWithToken(int userId) async { Future<bool> tryAuthenticateWithToken(int userId) async {
final storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
String? apiAuthToken = final apiAuthToken =
await storage.read(key: SecureStorageKeys.apiAuthToken); await storage.read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthToken != null) { if (apiAuthToken != null) {
@ -355,21 +358,21 @@ class ApiService {
final result = await sendRequestSync(req, authenticated: false); final result = await sendRequestSync(req, authenticated: false);
if (result.isSuccess) { if (result.isSuccess) {
server.Response_Ok ok = result.value; final ok = result.value as server.Response_Ok;
if (ok.hasAuthenticated()) { if (ok.hasAuthenticated()) {
server.Response_Authenticated authenticated = ok.authenticated; final authenticated = ok.authenticated;
updateUserdata((user) { await updateUserdata((user) {
user.subscriptionPlan = authenticated.plan; user.subscriptionPlan = authenticated.plan;
return user; return user;
}); });
} }
Log.info("websocket is authenticated"); Log.info('websocket is authenticated');
onAuthenticated(); unawaited(onAuthenticated());
return true; return true;
} }
if (result.isError) { if (result.isError) {
if (result.error != ErrorCode.AuthTokenNotValid) { if (result.error != ErrorCode.AuthTokenNotValid) {
Log.error("got error while authenticating to the server", result); Log.error('got error while authenticating to the server', result);
return false; return false;
} }
} }
@ -377,7 +380,7 @@ class ApiService {
return false; return false;
} }
Future authenticate() async { Future<void> authenticate() async {
if (isAuthenticated) return; if (isAuthenticated) return;
if (await getSignalIdentity() == null) { if (await getSignalIdentity() == null) {
return; return;
@ -392,11 +395,11 @@ class ApiService {
var handshake = Handshake() var handshake = Handshake()
..getauthchallenge = Handshake_GetAuthChallenge(); ..getauthchallenge = Handshake_GetAuthChallenge();
var req = createClientToServerFromHandshake(handshake); final req = createClientToServerFromHandshake(handshake);
final result = await sendRequestSync(req, authenticated: false); final result = await sendRequestSync(req, authenticated: false);
if (result.isError) { if (result.isError) {
Log.error("could not request auth challenge", result); Log.error('could not request auth challenge', result);
return; return;
} }
@ -405,7 +408,7 @@ class ApiService {
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey(); var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) return; if (privKey == null) return;
final random = getRandomUint8List(32); final random = getRandomUint8List(32);
final signature = sign(privKey.serialize(), challenge, random); final signature = sign(privKey.serialize(), challenge as Uint8List, random);
privKey = null; privKey = null;
final getAuthToken = Handshake_GetAuthToken() final getAuthToken = Handshake_GetAuthToken()
@ -414,18 +417,18 @@ class ApiService {
final getauthtoken = Handshake()..getauthtoken = getAuthToken; final getauthtoken = Handshake()..getauthtoken = getAuthToken;
var req2 = createClientToServerFromHandshake(getauthtoken); final req2 = createClientToServerFromHandshake(getauthtoken);
final result2 = await sendRequestSync(req2, authenticated: false); final result2 = await sendRequestSync(req2, authenticated: false);
if (result2.isError) { if (result2.isError) {
Log.error("could not send auth response: ${result2.error}"); Log.error('could not send auth response: ${result2.error}');
return; return;
} }
Uint8List apiAuthToken = result2.value.authtoken; final apiAuthToken = result2.value.authtoken as Uint8List;
String apiAuthTokenB64 = base64Encode(apiAuthToken); final apiAuthTokenB64 = base64Encode(apiAuthToken);
final storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
await storage.write( await storage.write(
key: SecureStorageKeys.apiAuthToken, value: apiAuthTokenB64); key: SecureStorageKeys.apiAuthToken, value: apiAuthTokenB64);
@ -452,51 +455,51 @@ class ApiService {
..signedPrekeyId = Int64(signedPreKey.id) ..signedPrekeyId = Int64(signedPreKey.id)
..isIos = Platform.isIOS; ..isIos = Platform.isIOS;
if (inviteCode != null && inviteCode != "") { if (inviteCode != null && inviteCode != '') {
register.inviteCode = inviteCode; register.inviteCode = inviteCode;
} }
var handshake = Handshake()..register = register; final handshake = Handshake()..register = register;
var req = createClientToServerFromHandshake(handshake); final req = createClientToServerFromHandshake(handshake);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> getUsername(int userId) async { Future<Result> getUsername(int userId) async {
var get = ApplicationData_GetUserById()..userId = Int64(userId); final get = ApplicationData_GetUserById()..userId = Int64(userId);
var appData = ApplicationData()..getuserbyid = get; final appData = ApplicationData()..getuserbyid = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, contactId: userId); return sendRequestSync(req, contactId: userId);
} }
Future<Result> downloadDone(List<int> token) async { Future<Result> downloadDone(List<int> token) async {
var get = ApplicationData_DownloadDone()..downloadToken = token; final get = ApplicationData_DownloadDone()..downloadToken = token;
var appData = ApplicationData()..downloaddone = get; final appData = ApplicationData()..downloaddone = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, ensureRetransmission: true); return sendRequestSync(req, ensureRetransmission: true);
} }
Future<Result> getCurrentLocation() async { Future<Result> getCurrentLocation() async {
var get = ApplicationData_GetLocation(); final get = ApplicationData_GetLocation();
var appData = ApplicationData()..getlocation = get; final appData = ApplicationData()..getlocation = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> getUserData(String username) async { Future<Result> getUserData(String username) async {
var get = ApplicationData_GetUserByUsername()..username = username; final get = ApplicationData_GetUserByUsername()..username = username;
var appData = ApplicationData()..getuserbyusername = get; final appData = ApplicationData()..getuserbyusername = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Response_PlanBallance?> getPlanBallance() async { Future<Response_PlanBallance?> getPlanBallance() async {
var get = ApplicationData_GetCurrentPlanInfos(); final get = ApplicationData_GetCurrentPlanInfos();
var appData = ApplicationData()..getcurrentplaninfos = get; final appData = ApplicationData()..getcurrentplaninfos = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req); final res = await sendRequestSync(req);
if (res.isSuccess) { if (res.isSuccess) {
server.Response_Ok ok = res.value; final ok = res.value as server.Response_Ok;
if (ok.hasPlanballance()) { if (ok.hasPlanballance()) {
return ok.planballance; return ok.planballance;
} }
@ -505,12 +508,12 @@ class ApiService {
} }
Future<Response_Vouchers?> getVoucherList() async { Future<Response_Vouchers?> getVoucherList() async {
var get = ApplicationData_GetVouchers(); final get = ApplicationData_GetVouchers();
var appData = ApplicationData()..getvouchers = get; final appData = ApplicationData()..getvouchers = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req); final res = await sendRequestSync(req);
if (res.isSuccess) { if (res.isSuccess) {
server.Response_Ok ok = res.value; final ok = res.value as server.Response_Ok;
if (ok.hasVouchers()) { if (ok.hasVouchers()) {
return ok.vouchers; return ok.vouchers;
} }
@ -519,12 +522,12 @@ class ApiService {
} }
Future<List<Response_AddAccountsInvite>?> getAdditionalUserInvites() async { Future<List<Response_AddAccountsInvite>?> getAdditionalUserInvites() async {
var get = ApplicationData_GetAddAccountsInvites(); final get = ApplicationData_GetAddAccountsInvites();
var appData = ApplicationData()..getaddaccountsinvites = get; final appData = ApplicationData()..getaddaccountsinvites = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req); final res = await sendRequestSync(req);
if (res.isSuccess) { if (res.isSuccess) {
server.Response_Ok ok = res.value; final ok = res.value as server.Response_Ok;
if (ok.hasAddaccountsinvites()) { if (ok.hasAddaccountsinvites()) {
return ok.addaccountsinvites.invites; return ok.addaccountsinvites.invites;
} }
@ -533,63 +536,63 @@ class ApiService {
} }
Future<Result> updatePlanOptions(bool autoRenewal) async { Future<Result> updatePlanOptions(bool autoRenewal) async {
var get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal; final get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal;
var appData = ApplicationData()..updateplanoptions = get; final appData = ApplicationData()..updateplanoptions = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> removeAdditionalUser(Int64 userId) async { Future<Result> removeAdditionalUser(Int64 userId) async {
var get = ApplicationData_RemoveAdditionalUser()..userId = userId; final get = ApplicationData_RemoveAdditionalUser()..userId = userId;
var appData = ApplicationData()..removeadditionaluser = get; final appData = ApplicationData()..removeadditionaluser = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, contactId: userId.toInt()); return sendRequestSync(req, contactId: userId.toInt());
} }
Future<Result> buyVoucher(int valueInCents) async { Future<Result> buyVoucher(int valueInCents) async {
var get = ApplicationData_CreateVoucher()..valueCents = valueInCents; final get = ApplicationData_CreateVoucher()..valueCents = valueInCents;
var appData = ApplicationData()..createvoucher = get; final appData = ApplicationData()..createvoucher = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> switchToPayedPlan( Future<Result> switchToPayedPlan(
String planId, bool payMonthly, bool autoRenewal) async { String planId, bool payMonthly, bool autoRenewal) async {
var get = ApplicationData_SwitchToPayedPlan() final get = ApplicationData_SwitchToPayedPlan()
..planId = planId ..planId = planId
..payMonthly = payMonthly ..payMonthly = payMonthly
..autoRenewal = autoRenewal; ..autoRenewal = autoRenewal;
var appData = ApplicationData()..switchtopayedplan = get; final appData = ApplicationData()..switchtopayedplan = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> redeemVoucher(String voucher) async { Future<Result> redeemVoucher(String voucher) async {
var get = ApplicationData_RedeemVoucher()..voucher = voucher; final get = ApplicationData_RedeemVoucher()..voucher = voucher;
var appData = ApplicationData()..redeemvoucher = get; final appData = ApplicationData()..redeemvoucher = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> deleteAccount() async { Future<Result> deleteAccount() async {
var get = ApplicationData_DeleteAccount(); final get = ApplicationData_DeleteAccount();
var appData = ApplicationData()..deleteaccount = get; final appData = ApplicationData()..deleteaccount = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> redeemUserInviteCode(String inviteCode) async { Future<Result> redeemUserInviteCode(String inviteCode) async {
var get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode; final get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode;
var appData = ApplicationData()..redeemadditionalcode = get; final appData = ApplicationData()..redeemadditionalcode = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> updateFCMToken(String googleFcm) async { Future<Result> updateFCMToken(String googleFcm) async {
var get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm;
var appData = ApplicationData()..updategooglefcmtoken = get; final appData = ApplicationData()..updategooglefcmtoken = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> updateSignedPreKey( Future<Result> updateSignedPreKey(
@ -597,22 +600,23 @@ class ApiService {
Uint8List signedPreKey, Uint8List signedPreKey,
Uint8List signedPreKeySignature, Uint8List signedPreKeySignature,
) async { ) async {
var get = ApplicationData_UpdateSignedPreKey() final get = ApplicationData_UpdateSignedPreKey()
..signedPrekeyId = Int64(signedPreKeyId) ..signedPrekeyId = Int64(signedPreKeyId)
..signedPrekey = signedPreKey ..signedPrekey = signedPreKey
..signedPrekeySignature = signedPreKeySignature; ..signedPrekeySignature = signedPreKeySignature;
var appData = ApplicationData()..updatesignedprekey = get; final appData = ApplicationData()..updatesignedprekey = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req); return sendRequestSync(req);
} }
Future<Response_SignedPreKey?> getSignedKeyByUserId(int userId) async { Future<Response_SignedPreKey?> getSignedKeyByUserId(int userId) async {
var get = ApplicationData_GetSignedPreKeyByUserId()..userId = Int64(userId); final get = ApplicationData_GetSignedPreKeyByUserId()
var appData = ApplicationData()..getsignedprekeybyuserid = get; ..userId = Int64(userId);
var req = createClientToServerFromApplicationData(appData); final appData = ApplicationData()..getsignedprekeybyuserid = get;
final req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req, contactId: userId); Result res = await sendRequestSync(req, contactId: userId);
if (res.isSuccess) { if (res.isSuccess) {
server.Response_Ok ok = res.value; final ok = res.value as server.Response_Ok;
if (ok.hasSignedprekey()) { if (ok.hasSignedprekey()) {
return ok.signedprekey; return ok.signedprekey;
} }
@ -621,12 +625,12 @@ class ApiService {
} }
Future<OtherPreKeys?> getPreKeysByUserId(int userId) async { Future<OtherPreKeys?> getPreKeysByUserId(int userId) async {
var get = ApplicationData_GetPrekeysByUserId()..userId = Int64(userId); final get = ApplicationData_GetPrekeysByUserId()..userId = Int64(userId);
var appData = ApplicationData()..getprekeysbyuserid = get; final appData = ApplicationData()..getprekeysbyuserid = get;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req, contactId: userId); Result res = await sendRequestSync(req, contactId: userId);
if (res.isSuccess) { if (res.isSuccess) {
server.Response_Ok ok = res.value; final ok = res.value as server.Response_Ok;
if (ok.hasUserdata()) { if (ok.hasUserdata()) {
server.Response_UserData data = ok.userdata; server.Response_UserData data = ok.userdata;
if (data.hasSignedPrekey() && if (data.hasSignedPrekey() &&
@ -654,8 +658,8 @@ class ApiService {
testMessage.pushData = pushData; testMessage.pushData = pushData;
} }
var appData = ApplicationData()..textmessage = testMessage; final appData = ApplicationData()..textmessage = testMessage;
var req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, contactId: target); return sendRequestSync(req, contactId: target);
} }
} }

View file

@ -1,18 +1,20 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -20,13 +22,13 @@ import 'package:twonly/src/views/camera/share_image_editor_view.dart';
Map<int, DateTime> downloadStartedForMediaReceived = {}; Map<int, DateTime> downloadStartedForMediaReceived = {};
Future tryDownloadAllMediaFiles({bool force = false}) async { Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
// This is called when WebSocket is newly connected, so allow all downloads to be restarted. // This is called when WebSocket is newly connected, so allow all downloads to be restarted.
downloadStartedForMediaReceived = {}; downloadStartedForMediaReceived = {};
List<Message> messages = final messages =
await twonlyDB.messagesDao.getAllMessagesPendingDownloading(); await twonlyDB.messagesDao.getAllMessagesPendingDownloading();
for (Message message in messages) { for (final message in messages) {
await startDownloadMedia(message, force); await startDownloadMedia(message, force);
} }
} }
@ -45,8 +47,7 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
}; };
Future<bool> isAllowedToDownload(bool isVideo) async { Future<bool> isAllowedToDownload(bool isVideo) async {
final List<ConnectivityResult> connectivityResult = final connectivityResult = await Connectivity().checkConnectivity();
await (Connectivity().checkConnectivity());
final user = await getUser(); final user = await getUser();
final options = user!.autoDownloadOptions ?? defaultAutoDownloadOptions; final options = user!.autoDownloadOptions ?? defaultAutoDownloadOptions;
@ -76,9 +77,9 @@ Future<bool> isAllowedToDownload(bool isVideo) async {
return false; return false;
} }
Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
int messageId = int.parse(update.task.taskId.replaceAll("download_", "")); final messageId = int.parse(update.task.taskId.replaceAll('download_', ''));
bool failed = false; var failed = false;
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled || update.status == TaskStatus.canceled ||
@ -90,46 +91,47 @@ Future handleDownloadStatusUpdate(TaskStatusUpdate update) async {
} else { } else {
failed = true; failed = true;
Log.error( Log.error(
"Got invalid response status code: ${update.responseStatusCode}", 'Got invalid response status code: ${update.responseStatusCode}',
); );
} }
} else { } else {
Log.info("Got ${update.status} for $messageId"); Log.info('Got ${update.status} for $messageId');
return; return;
} }
await handleDownloadStatusUpdateInternal(messageId, failed); await handleDownloadStatusUpdateInternal(messageId, failed);
} }
Future handleDownloadStatusUpdateInternal(int messageId, bool failed) async { Future<void> handleDownloadStatusUpdateInternal(
int messageId, bool failed) async {
if (failed) { if (failed) {
Log.error("Download failed for $messageId"); Log.error('Download failed for $messageId');
Message? message = await twonlyDB.messagesDao final message = await twonlyDB.messagesDao
.getMessageByMessageId(messageId) .getMessageByMessageId(messageId)
.getSingleOrNull(); .getSingleOrNull();
if (message != null) { if (message != null) {
await handleMediaError(message); await handleMediaError(message);
} }
} else { } else {
Log.info("Download was successfully for $messageId"); Log.info('Download was successfully for $messageId');
await handleEncryptedFile(messageId); await handleEncryptedFile(messageId);
} }
} }
Future startDownloadMedia(Message message, bool force, Future<void> startDownloadMedia(Message message, bool force,
{int retryCounter = 0}) async { {int retryCounter = 0}) async {
if (message.contentJson == null) return; if (message.contentJson == null) return;
if (downloadStartedForMediaReceived[message.messageId] != null && if (downloadStartedForMediaReceived[message.messageId] != null &&
retryCounter == 0) { retryCounter == 0) {
DateTime started = downloadStartedForMediaReceived[message.messageId]!; final started = downloadStartedForMediaReceived[message.messageId]!;
Duration elapsed = DateTime.now().difference(started); final elapsed = DateTime.now().difference(started);
if (elapsed <= Duration(seconds: 60)) { if (elapsed <= const Duration(seconds: 60)) {
Log.error("Download already started..."); Log.error('Download already started...');
return; return;
} }
} }
final content = final content = MessageContent.fromJson(
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!)); message.kind, jsonDecode(message.contentJson!) as Map);
if (content is! MediaMessageContent) return; if (content is! MediaMessageContent) return;
if (content.downloadToken == null) return; if (content.downloadToken == null) return;
@ -158,7 +160,7 @@ Future startDownloadMedia(Message message, bool force,
if (message.downloadState != DownloadState.downloaded) { if (message.downloadState != DownloadState.downloaded) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId, message.messageId,
MessagesCompanion( const MessagesCompanion(
downloadState: Value(DownloadState.downloading), downloadState: Value(DownloadState.downloading),
), ),
); );
@ -166,36 +168,34 @@ Future startDownloadMedia(Message message, bool force,
downloadStartedForMediaReceived[message.messageId] = DateTime.now(); downloadStartedForMediaReceived[message.messageId] = DateTime.now();
String downloadToken = uint8ListToHex(content.downloadToken!); final downloadToken = uint8ListToHex(content.downloadToken!);
String apiUrl = final apiUrl =
"http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken"; 'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken';
try { try {
final task = DownloadTask( final task = DownloadTask(
url: apiUrl, url: apiUrl,
taskId: "download_${media.messageId}", taskId: 'download_${media.messageId}',
directory: "media/received/", directory: 'media/received/',
baseDirectory: BaseDirectory.applicationSupport, baseDirectory: BaseDirectory.applicationSupport,
filename: "${media.messageId}.encrypted", filename: '${media.messageId}.encrypted',
priority: 0, priority: 0,
retries: 10, retries: 10,
); );
Log.info( Log.info(
"Got media file. Starting download: ${downloadToken.substring(0, 10)}", 'Got media file. Starting download: ${downloadToken.substring(0, 10)}',
); );
try { try {
await downloadFileFast(media.messageId, apiUrl); await downloadFileFast(media.messageId, apiUrl);
return;
} catch (e) { } catch (e) {
Log.error("Fast download failed: $e"); Log.error('Fast download failed: $e');
final result = await FileDownloader().enqueue(task); await FileDownloader().enqueue(task);
return result;
} }
} catch (e) { } catch (e) {
Log.error("Exception during download: $e"); Log.error('Exception during download: $e');
} }
} }
@ -203,19 +203,19 @@ Future<void> downloadFileFast(
int messageId, int messageId,
String apiUrl, String apiUrl,
) async { ) async {
final String directoryPath = final directoryPath =
"${(await getApplicationSupportDirectory()).path}/media/received/"; '${(await getApplicationSupportDirectory()).path}/media/received/';
final String filename = "$messageId.encrypted"; final filename = '$messageId.encrypted';
final Directory directory = Directory(directoryPath); final directory = Directory(directoryPath);
if (!await directory.exists()) { if (!directory.existsSync()) {
await directory.create(recursive: true); await directory.create(recursive: true);
} }
final String filePath = "${directory.path}/$filename"; final filePath = '${directory.path}/$filename';
final response = final response =
await http.get(Uri.parse(apiUrl)).timeout(Duration(seconds: 10)); await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10));
if (response.statusCode == 200) { if (response.statusCode == 200) {
await File(filePath).writeAsBytes(response.bodyBytes); await File(filePath).writeAsBytes(response.bodyBytes);
@ -228,34 +228,34 @@ Future<void> downloadFileFast(
return; return;
} }
// can be tried again // can be tried again
throw Exception("Fast download failed with status: ${response.statusCode}"); throw Exception('Fast download failed with status: ${response.statusCode}');
} }
} }
Future handleEncryptedFile(int messageId) async { Future<void> handleEncryptedFile(int messageId) async {
Message? msg = await twonlyDB.messagesDao final msg = await twonlyDB.messagesDao
.getMessageByMessageId(messageId) .getMessageByMessageId(messageId)
.getSingleOrNull(); .getSingleOrNull();
if (msg == null) { if (msg == null) {
Log.error("Not message for downloaded file found: $messageId"); Log.error('Not message for downloaded file found: $messageId');
return; return;
} }
Uint8List? encryptedBytes = await readMediaFile(msg.messageId, "encrypted"); final encryptedBytes = await readMediaFile(msg.messageId, 'encrypted');
if (encryptedBytes == null) { if (encryptedBytes == null) {
Log.error("encrypted bytes are not found for ${msg.messageId}"); Log.error('encrypted bytes are not found for ${msg.messageId}');
return; return;
} }
MediaMessageContent content = final content =
MediaMessageContent.fromJson(jsonDecode(msg.contentJson!)); MediaMessageContent.fromJson(jsonDecode(msg.contentJson!) as Map);
try { try {
final chacha20 = FlutterChacha20.poly1305Aead(); final chacha20 = FlutterChacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!); final secretKeyData = SecretKeyData(content.encryptionKey!);
SecretBox secretBox = SecretBox( final secretBox = SecretBox(
encryptedBytes, encryptedBytes,
nonce: content.encryptionNonce!, nonce: content.encryptionNonce!,
mac: Mac(content.encryptionMac!), mac: Mac(content.encryptionMac!),
@ -269,10 +269,10 @@ Future handleEncryptedFile(int messageId) async {
if (content.isVideo) { if (content.isVideo) {
final extractedBytes = extractUint8Lists(imageBytes); final extractedBytes = extractUint8Lists(imageBytes);
imageBytes = extractedBytes[0]; imageBytes = extractedBytes[0];
await writeMediaFile(msg.messageId, "mp4", extractedBytes[1]); await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]);
} }
await writeMediaFile(msg.messageId, "png", imageBytes); await writeMediaFile(msg.messageId, 'png', imageBytes);
// } catch (e) { // } catch (e) {
// Log.error( // Log.error(
// "could not decrypt the media file in the second try. reporting error to user: $e"); // "could not decrypt the media file in the second try. reporting error to user: $e");
@ -280,13 +280,13 @@ Future handleEncryptedFile(int messageId) async {
// return; // return;
// } // }
} catch (e) { } catch (e) {
Log.error("$e"); Log.error('$e');
/// legacy support /// legacy support
final chacha20 = Xchacha20.poly1305Aead(); final chacha20 = Xchacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!); final secretKeyData = SecretKeyData(content.encryptionKey!);
SecretBox secretBox = SecretBox( final secretBox = SecretBox(
encryptedBytes, encryptedBytes,
nonce: content.encryptionNonce!, nonce: content.encryptionNonce!,
mac: Mac(content.encryptionMac!), mac: Mac(content.encryptionMac!),
@ -300,121 +300,122 @@ Future handleEncryptedFile(int messageId) async {
if (content.isVideo) { if (content.isVideo) {
final extractedBytes = extractUint8Lists(imageBytes); final extractedBytes = extractUint8Lists(imageBytes);
imageBytes = extractedBytes[0]; imageBytes = extractedBytes[0];
await writeMediaFile(msg.messageId, "mp4", extractedBytes[1]); await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]);
} }
await writeMediaFile(msg.messageId, "png", imageBytes); await writeMediaFile(msg.messageId, 'png', imageBytes);
} catch (e) { } catch (e) {
Log.error( Log.error(
"could not decrypt the media file in the second try. reporting error to user: $e"); 'could not decrypt the media file in the second try. reporting error to user: $e');
handleMediaError(msg); await handleMediaError(msg);
return; return;
} }
} }
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
msg.messageId, msg.messageId,
MessagesCompanion(downloadState: Value(DownloadState.downloaded)), const MessagesCompanion(downloadState: Value(DownloadState.downloaded)),
); );
Log.info("Download and decryption of ${msg.messageId} was successful"); Log.info('Download and decryption of ${msg.messageId} was successful');
await deleteMediaFile(msg.messageId, "encrypted"); await deleteMediaFile(msg.messageId, 'encrypted');
apiService.downloadDone(content.downloadToken!); unawaited(apiService.downloadDone(content.downloadToken!));
} }
Future<Uint8List?> getImageBytes(int mediaId) async { Future<Uint8List?> getImageBytes(int mediaId) async {
return await readMediaFile(mediaId, "png"); return readMediaFile(mediaId, 'png');
} }
Future<File?> getVideoPath(int mediaId) async { Future<File?> getVideoPath(int mediaId) async {
String basePath = await getMediaFilePath(mediaId, "received"); final basePath = await getMediaFilePath(mediaId, 'received');
return File("$basePath.mp4"); return File('$basePath.mp4');
} }
/// --- helper functions --- /// --- helper functions ---
Future<Uint8List?> readMediaFile(int mediaId, String type) async { Future<Uint8List?> readMediaFile(int mediaId, String type) async {
String basePath = await getMediaFilePath(mediaId, "received"); final basePath = await getMediaFilePath(mediaId, 'received');
File file = File("$basePath.$type"); final file = File('$basePath.$type');
Log.info("Reading: $file"); Log.info('Reading: $file');
if (!await file.exists()) { if (!file.existsSync()) {
return null; return null;
} }
return await file.readAsBytes(); return file.readAsBytes();
} }
Future<bool> existsMediaFile(int mediaId, String type) async { Future<bool> existsMediaFile(int mediaId, String type) async {
String basePath = await getMediaFilePath(mediaId, "received"); final basePath = await getMediaFilePath(mediaId, 'received');
File file = File("$basePath.$type"); final file = File('$basePath.$type');
return await file.exists(); return file.existsSync();
} }
Future<void> writeMediaFile(int mediaId, String type, Uint8List data) async { Future<void> writeMediaFile(int mediaId, String type, Uint8List data) async {
String basePath = await getMediaFilePath(mediaId, "received"); final basePath = await getMediaFilePath(mediaId, 'received');
File file = File("$basePath.$type"); final file = File('$basePath.$type');
await file.writeAsBytes(data); await file.writeAsBytes(data);
} }
Future<void> deleteMediaFile(int mediaId, String type) async { Future<void> deleteMediaFile(int mediaId, String type) async {
String basePath = await getMediaFilePath(mediaId, "received"); final basePath = await getMediaFilePath(mediaId, 'received');
File file = File("$basePath.$type"); final file = File('$basePath.$type');
try { try {
if (await file.exists()) { if (file.existsSync()) {
await file.delete(); await file.delete();
} }
} catch (e) { } catch (e) {
Log.error("Error deleting: $e"); Log.error('Error deleting: $e');
} }
} }
Future<void> purgeReceivedMediaFiles() async { Future<void> purgeReceivedMediaFiles() async {
final basedir = await getApplicationSupportDirectory(); final basedir = await getApplicationSupportDirectory();
final directory = Directory(join(basedir.path, 'media', "received")); final directory = Directory(join(basedir.path, 'media', 'received'));
await purgeMediaFiles(directory); await purgeMediaFiles(directory);
} }
Future<void> purgeMediaFiles(Directory directory) async { Future<void> purgeMediaFiles(Directory directory) async {
// Check if the directory exists // Check if the directory exists
if (await directory.exists()) { if (directory.existsSync()) {
// List all files in the directory // List all files in the directory
List<FileSystemEntity> files = directory.listSync(); final files = directory.listSync();
// Iterate over each file // Iterate over each file
for (var file in files) { for (final file in files) {
// Get the filename // Get the filename
String filename = file.uri.pathSegments.last; final filename = file.uri.pathSegments.last;
// Use a regular expression to extract the integer part // Use a regular expression to extract the integer part
final match = RegExp(r'(\d+)').firstMatch(filename); final match = RegExp(r'(\d+)').firstMatch(filename);
if (match != null) { if (match != null) {
// Parse the integer and add it to the list // Parse the integer and add it to the list
int fileId = int.parse(match.group(0)!); final fileId = int.parse(match.group(0)!);
try { try {
if (directory.path.endsWith("send")) { if (directory.path.endsWith('send')) {
List<Message> messages = final messages =
await twonlyDB.messagesDao.getMessagesByMediaUploadId(fileId); await twonlyDB.messagesDao.getMessagesByMediaUploadId(fileId);
bool canBeDeleted = true; var canBeDeleted = true;
for (final message in messages) { for (final message in messages) {
try { try {
MediaMessageContent content = MediaMessageContent.fromJson( final content = MediaMessageContent.fromJson(
jsonDecode(message.contentJson!), jsonDecode(message.contentJson!) as Map,
); );
DateTime oneDayAgo = DateTime.now().subtract(Duration(days: 1)); final oneDayAgo =
DateTime twoDaysAgo = DateTime.now().subtract(const Duration(days: 1));
DateTime.now().subtract(Duration(days: 1)); final twoDaysAgo =
DateTime.now().subtract(const Duration(days: 1));
if (((message.openedAt == null || if ((message.openedAt == null ||
oneDayAgo.isBefore(message.openedAt!)) && oneDayAgo.isBefore(message.openedAt!)) &&
!message.errorWhileSending)) { !message.errorWhileSending) {
canBeDeleted = false; canBeDeleted = false;
} else if (message.mediaStored) { } else if (message.mediaStored) {
if (!file.path.contains(".original.") && if (!file.path.contains('.original.') &&
!file.path.contains(".encrypted")) { !file.path.contains('.encrypted')) {
canBeDeleted = false; canBeDeleted = false;
} }
} }
@ -429,8 +430,8 @@ Future<void> purgeMediaFiles(Directory directory) async {
canBeDeleted = true; canBeDeleted = true;
} }
// Encrypted or upload data can be removed when acknowledgeByServer // Encrypted or upload data can be removed when acknowledgeByServer
if (file.path.contains(".upload") || if (file.path.contains('.upload') ||
file.path.contains(".encrypted")) { file.path.contains('.encrypted')) {
canBeDeleted = true; canBeDeleted = true;
} }
} }
@ -439,11 +440,11 @@ Future<void> purgeMediaFiles(Directory directory) async {
} }
} }
if (canBeDeleted) { if (canBeDeleted) {
Log.info("purged media file ${file.path} "); Log.info('purged media file ${file.path} ');
file.deleteSync(); file.deleteSync();
} }
} else { } else {
Message? message = await twonlyDB.messagesDao final message = await twonlyDB.messagesDao
.getMessageByMessageId(fileId) .getMessageByMessageId(fileId)
.getSingleOrNull(); .getSingleOrNull();
if ((message == null) || if ((message == null) ||
@ -455,7 +456,7 @@ Future<void> purgeMediaFiles(Directory directory) async {
} }
} }
} catch (e) { } catch (e) {
Log.error("$e"); Log.error('$e');
} }
} }
} }

View file

@ -1,15 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:io';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -35,11 +36,11 @@ import 'package:video_compress/video_compress.dart';
Future<ErrorCode?> isAllowedToSend() async { Future<ErrorCode?> isAllowedToSend() async {
final user = await getUser(); final user = await getUser();
if (user == null) return null; if (user == null) return null;
if (user.subscriptionPlan == "Preview") { if (user.subscriptionPlan == 'Preview') {
return ErrorCode.PlanNotAllowed; return ErrorCode.PlanNotAllowed;
} }
if (user.subscriptionPlan == "Free") { if (user.subscriptionPlan == 'Free') {
int? todaysImageCounter = user.todaysImageCounter; var todaysImageCounter = user.todaysImageCounter;
if (user.lastImageSend != null && user.todaysImageCounter != null) { if (user.lastImageSend != null && user.todaysImageCounter != null) {
if (isToday(user.lastImageSend!)) { if (isToday(user.lastImageSend!)) {
if (user.todaysImageCounter == 3) { if (user.todaysImageCounter == 3) {
@ -53,25 +54,26 @@ Future<ErrorCode?> isAllowedToSend() async {
todaysImageCounter = 1; todaysImageCounter = 1;
} }
await updateUserdata((user) { await updateUserdata((user) {
user.lastImageSend = DateTime.now(); user
user.todaysImageCounter = todaysImageCounter; ..lastImageSend = DateTime.now()
..todaysImageCounter = todaysImageCounter;
return user; return user;
}); });
} }
return null; return null;
} }
Future initFileDownloader() async { Future<void> initFileDownloader() async {
FileDownloader().updates.listen((update) async { FileDownloader().updates.listen((update) async {
switch (update) { switch (update) {
case TaskStatusUpdate(): case TaskStatusUpdate():
if (update.task.taskId.contains("upload_")) { if (update.task.taskId.contains('upload_')) {
await handleUploadStatusUpdate(update); await handleUploadStatusUpdate(update);
} }
if (update.task.taskId.contains("download_")) { if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update); await handleDownloadStatusUpdate(update);
} }
if (update.task.taskId.contains("backup")) { if (update.task.taskId.contains('backup')) {
await handleBackupStatusUpdate(update); await handleBackupStatusUpdate(update);
} }
case TaskProgressUpdate(): case TaskProgressUpdate():
@ -82,17 +84,16 @@ Future initFileDownloader() async {
await FileDownloader().start(); await FileDownloader().start();
FileDownloader().configure(androidConfig: [ await FileDownloader().configure(androidConfig: [
(Config.bypassTLSCertificateValidation, kDebugMode), (Config.bypassTLSCertificateValidation, kDebugMode),
]); ]);
if (kDebugMode) { if (kDebugMode) {
FileDownloader().configureNotification( FileDownloader().configureNotification(
running: TaskNotification( running: const TaskNotification(
'Uploading/Downloading', 'Uploading/Downloading',
'{filename} ({progress}).', '{filename} ({progress}).',
), ),
complete: null,
progressBar: true, progressBar: true,
); );
} }
@ -112,14 +113,14 @@ Future initFileDownloader() async {
Future<bool> checkForFailedUploads() async { Future<bool> checkForFailedUploads() async {
final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload();
List<int> mediaUploadIds = []; final mediaUploadIds = <int>[];
for (Message message in messages) { for (var message in messages) {
if (mediaUploadIds.contains(message.mediaUploadId)) { if (mediaUploadIds.contains(message.mediaUploadId)) {
continue; continue;
} }
int affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload( final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
message.mediaUploadId!, message.mediaUploadId!,
MediaUploadsCompanion( const MediaUploadsCompanion(
state: Value(UploadState.pending), state: Value(UploadState.pending),
encryptionData: Value( encryptionData: Value(
null, // start from scratch e.q. encrypt the files again if already happen null, // start from scratch e.q. encrypt the files again if already happen
@ -128,11 +129,11 @@ Future<bool> checkForFailedUploads() async {
); );
if (affectedRows == 0) { if (affectedRows == 0) {
Log.error( Log.error(
"The media from message ${message.messageId} already deleted.", 'The media from message ${message.messageId} already deleted.',
); );
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId, message.messageId,
MessagesCompanion( const MessagesCompanion(
errorWhileSending: Value(true), errorWhileSending: Value(true),
), ),
); );
@ -142,24 +143,24 @@ Future<bool> checkForFailedUploads() async {
} }
if (messages.isNotEmpty) { if (messages.isNotEmpty) {
Log.error( Log.error(
"Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.", 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.',
); );
} }
return mediaUploadIds.isNotEmpty; // return true if there are affected return mediaUploadIds.isNotEmpty; // return true if there are affected
} }
final lockingHandleMediaFile = Mutex(); final lockingHandleMediaFile = Mutex();
Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
if (maxRetries == 0) { if (maxRetries == 0) {
Log.error("retried media upload 3 times. abort retrying"); Log.error('retried media upload 3 times. abort retrying');
return; return;
} }
bool retry = await lockingHandleMediaFile.protect<bool>(() async { final retry = await lockingHandleMediaFile.protect<bool>(() async {
final mediaFiles = await twonlyDB.mediaUploadsDao.getMediaUploadsForRetry(); final mediaFiles = await twonlyDB.mediaUploadsDao.getMediaUploadsForRetry();
if (mediaFiles.isEmpty) { if (mediaFiles.isEmpty) {
return checkForFailedUploads(); return checkForFailedUploads();
} }
Log.info("re uploading ${mediaFiles.length} media files."); Log.info('re uploading ${mediaFiles.length} media files.');
for (final mediaFile in mediaFiles) { for (final mediaFile in mediaFiles) {
if (mediaFile.messageIds == null || mediaFile.metadata == null) { if (mediaFile.messageIds == null || mediaFile.metadata == null) {
if (appRestarted) { if (appRestarted) {
@ -168,7 +169,7 @@ Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
await twonlyDB.mediaUploadsDao await twonlyDB.mediaUploadsDao
.deleteMediaUpload(mediaFile.mediaUploadId); .deleteMediaUpload(mediaFile.mediaUploadId);
Log.info( Log.info(
"upload can be removed, the finalized function was never called...", 'upload can be removed, the finalized function was never called...',
); );
} }
continue; continue;
@ -188,24 +189,23 @@ Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
} }
Future<int?> initMediaUpload() async { Future<int?> initMediaUpload() async {
return await twonlyDB.mediaUploadsDao return twonlyDB.mediaUploadsDao
.insertMediaUpload(MediaUploadsCompanion()); .insertMediaUpload(const MediaUploadsCompanion());
} }
Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async { Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
String basePath = await getMediaFilePath(mediaUploadId, "send"); final basePath = await getMediaFilePath(mediaUploadId, 'send');
await videoFilePath.copy("$basePath.original.mp4"); await videoFilePath.copy('$basePath.original.mp4');
return await compressVideoIfExists(mediaUploadId); return compressVideoIfExists(mediaUploadId);
} }
Future<Uint8List> addOrModifyImageToUpload( Future<Uint8List> addOrModifyImageToUpload(
int mediaUploadId, Uint8List imageBytes) async { int mediaUploadId, Uint8List imageBytes) async {
Uint8List imageBytesCompressed; Uint8List imageBytesCompressed;
Stopwatch stopwatch = Stopwatch(); final stopwatch = Stopwatch()..start();
stopwatch.start();
Log.info("Raw images size in bytes: ${imageBytes.length}"); Log.info('Raw images size in bytes: ${imageBytes.length}');
try { try {
imageBytesCompressed = await FlutterImageCompress.compressWithList( imageBytesCompressed = await FlutterImageCompress.compressWithList(
@ -224,11 +224,11 @@ Future<Uint8List> addOrModifyImageToUpload(
quality: 60, quality: 60,
); );
} }
await writeSendMediaFile(mediaUploadId, "png", imageBytesCompressed); await writeSendMediaFile(mediaUploadId, 'png', imageBytesCompressed);
} catch (e) { } catch (e) {
Log.error("$e"); Log.error('$e');
// as a fall back use the original image // as a fall back use the original image
await writeSendMediaFile(mediaUploadId, "png", imageBytes); await writeSendMediaFile(mediaUploadId, 'png', imageBytes);
imageBytesCompressed = imageBytes; imageBytesCompressed = imageBytes;
} }
@ -236,7 +236,7 @@ Future<Uint8List> addOrModifyImageToUpload(
Log.info( Log.info(
'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds'); 'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds');
Log.info("Raw images size in bytes: ${imageBytesCompressed.length}"); Log.info('Raw images size in bytes: ${imageBytesCompressed.length}');
// stopwatch.reset(); // stopwatch.reset();
// stopwatch.start(); // stopwatch.start();
@ -256,16 +256,16 @@ Future<Uint8List> addOrModifyImageToUpload(
/// remove the data so it will be done again. /// remove the data so it will be done again.
await twonlyDB.mediaUploadsDao.updateMediaUpload( await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId, mediaUploadId,
MediaUploadsCompanion( const MediaUploadsCompanion(
encryptionData: Value(null), encryptionData: Value(null),
), ),
); );
return imageBytesCompressed; return imageBytesCompressed;
} }
Future handlePreProcessingState(MediaUpload media) async { Future<void> handlePreProcessingState(MediaUpload media) async {
try { try {
final imageHandler = readSendMediaFile(media.mediaUploadId, "png"); final imageHandler = readSendMediaFile(media.mediaUploadId, 'png');
final videoHandler = compressVideoIfExists(media.mediaUploadId); final videoHandler = compressVideoIfExists(media.mediaUploadId);
await encryptMediaFiles( await encryptMediaFiles(
media.mediaUploadId, media.mediaUploadId,
@ -273,34 +273,36 @@ Future handlePreProcessingState(MediaUpload media) async {
videoHandler, videoHandler,
); );
} catch (e) { } catch (e) {
Log.error("${media.mediaUploadId} got error in pre processing: $e"); Log.error('${media.mediaUploadId} got error in pre processing: $e');
await handleUploadError(media); await handleUploadError(media);
} }
} }
Future encryptMediaFiles( Future<void> encryptMediaFiles(
int mediaUploadId, int mediaUploadId,
Future imageHandler, Future<Uint8List> imageHandler,
Future<bool>? videoHandler, Future<bool>? videoHandler,
) async { ) async {
Log.info("$mediaUploadId encrypting files"); Log.info('$mediaUploadId encrypting files');
Uint8List dataToEncrypt = await imageHandler; // ignore: cast_nullable_to_non_nullable
var dataToEncrypt = await imageHandler;
/// if there is a video wait until it is finished with compression /// if there is a video wait until it is finished with compression
if (videoHandler != null) { if (videoHandler != null) {
if (await videoHandler) { if (await videoHandler) {
Uint8List compressedVideo = await readSendMediaFile(mediaUploadId, "mp4"); final compressedVideo = await readSendMediaFile(mediaUploadId, 'mp4');
dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo); dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo);
} }
} }
var state = MediaEncryptionData(); final state = MediaEncryptionData();
final chacha20 = FlutterChacha20.poly1305Aead(); final chacha20 = FlutterChacha20.poly1305Aead();
SecretKeyData secretKey = await (await chacha20.newSecretKey()).extract(); final secretKey = await (await chacha20.newSecretKey()).extract();
state.encryptionKey = secretKey.bytes; state
state.encryptionNonce = chacha20.newNonce(); ..encryptionKey = secretKey.bytes
..encryptionNonce = chacha20.newNonce();
final secretBox = await chacha20.encrypt( final secretBox = await chacha20.encrypt(
dataToEncrypt, dataToEncrypt,
@ -308,46 +310,46 @@ Future encryptMediaFiles(
nonce: state.encryptionNonce, nonce: state.encryptionNonce,
); );
state.encryptionMac = secretBox.mac.bytes; state
..encryptionMac = secretBox.mac.bytes
state.sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes; ..sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes;
final encryptedBytes = Uint8List.fromList(secretBox.cipherText); final encryptedBytes = Uint8List.fromList(secretBox.cipherText);
await writeSendMediaFile( await writeSendMediaFile(
mediaUploadId, mediaUploadId,
"encrypted", 'encrypted',
encryptedBytes, encryptedBytes,
); );
await twonlyDB.mediaUploadsDao.updateMediaUpload( await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId, mediaUploadId,
MediaUploadsCompanion( MediaUploadsCompanion(
state: Value(UploadState.readyToUpload), state: const Value(UploadState.readyToUpload),
encryptionData: Value(state), encryptionData: Value(state),
), ),
); );
handleNextMediaUploadSteps(mediaUploadId); unawaited(handleNextMediaUploadSteps(mediaUploadId));
} }
Future finalizeUpload(int mediaUploadId, List<int> contactIds, Future<void> finalizeUpload(int mediaUploadId, List<int> contactIds,
bool isRealTwonly, bool isVideo, bool mirrorVideo, int maxShowTime) async { bool isRealTwonly, bool isVideo, bool mirrorVideo, int maxShowTime) async {
MediaUploadMetadata metadata = MediaUploadMetadata(); final metadata = MediaUploadMetadata()
metadata.contactIds = contactIds; ..contactIds = contactIds
metadata.isRealTwonly = isRealTwonly; ..isRealTwonly = isRealTwonly
metadata.messageSendAt = DateTime.now(); ..messageSendAt = DateTime.now()
metadata.isVideo = isVideo; ..isVideo = isVideo
metadata.maxShowTime = maxShowTime; ..maxShowTime = maxShowTime
metadata.mirrorVideo = mirrorVideo; ..mirrorVideo = mirrorVideo;
List<int> messageIds = []; final messageIds = <int>[];
for (final contactId in contactIds) { for (final contactId in contactIds) {
int? messageId = await twonlyDB.messagesDao.insertMessage( final messageId = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
contactId: Value(contactId), contactId: Value(contactId),
kind: Value(MessageKind.media), kind: const Value(MessageKind.media),
sendAt: Value(metadata.messageSendAt), sendAt: Value(metadata.messageSendAt),
downloadState: Value(DownloadState.pending), downloadState: const Value(DownloadState.pending),
mediaUploadId: Value(mediaUploadId), mediaUploadId: Value(mediaUploadId),
contentJson: Value( contentJson: Value(
jsonEncode( jsonEncode(
@ -364,14 +366,14 @@ Future finalizeUpload(int mediaUploadId, List<int> contactIds,
// de-archive contact when sending a new message // de-archive contact when sending a new message
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
contactId, contactId,
ContactsCompanion( const ContactsCompanion(
archived: Value(false), archived: Value(false),
), ),
); );
if (messageId != null) { if (messageId != null) {
messageIds.add(messageId); messageIds.add(messageId);
} else { } else {
Log.error("Error inserting media upload message in database."); Log.error('Error inserting media upload message in database.');
} }
} }
@ -383,13 +385,13 @@ Future finalizeUpload(int mediaUploadId, List<int> contactIds,
), ),
); );
handleNextMediaUploadSteps(mediaUploadId); unawaited(handleNextMediaUploadSteps(mediaUploadId));
} }
final lockingHandleNextMediaUploadStep = Mutex(); final lockingHandleNextMediaUploadStep = Mutex();
Future handleNextMediaUploadSteps(int mediaUploadId) async { Future<void> handleNextMediaUploadSteps(int mediaUploadId) async {
await lockingHandleNextMediaUploadStep.protect(() async { await lockingHandleNextMediaUploadStep.protect(() async {
var mediaUpload = await twonlyDB.mediaUploadsDao final mediaUpload = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId) .getMediaUploadById(mediaUploadId)
.getSingleOrNull(); .getSingleOrNull();
@ -397,7 +399,7 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
if (mediaUpload.state == UploadState.receiverNotified || if (mediaUpload.state == UploadState.receiverNotified ||
mediaUpload.state == UploadState.uploadTaskStarted) { mediaUpload.state == UploadState.uploadTaskStarted) {
/// Upload done and all users are notified :) /// Upload done and all users are notified :)
Log.info("$mediaUploadId is already done"); Log.info('$mediaUploadId is already done');
return false; return false;
} }
try { try {
@ -414,7 +416,7 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
await handleMediaUpload(mediaUpload); await handleMediaUpload(mediaUpload);
} catch (e) { } catch (e) {
Log.error("Non recoverable error while sending media file: $e"); Log.error('Non recoverable error while sending media file: $e');
await handleUploadError(mediaUpload); await handleUploadError(mediaUpload);
} }
return false; return false;
@ -427,22 +429,22 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
/// ///
/// ///
Future handleUploadStatusUpdate(TaskStatusUpdate update) async { Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
bool failed = false; var failed = false;
int mediaUploadId = int.parse(update.task.taskId.replaceAll("upload_", "")); final mediaUploadId = int.parse(update.task.taskId.replaceAll('upload_', ''));
MediaUpload? media = await twonlyDB.mediaUploadsDao final media = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId) .getMediaUploadById(mediaUploadId)
.getSingleOrNull(); .getSingleOrNull();
if (media == null) { if (media == null) {
Log.error( Log.error(
"Got an upload task but no upload media in the media upload database", 'Got an upload task but no upload media in the media upload database',
); );
return; return;
} }
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
Log.error("Upload failed: ${update.status}"); Log.error('Upload failed: ${update.status}');
failed = true; failed = true;
} else if (update.status == TaskStatus.complete) { } else if (update.status == TaskStatus.complete) {
if (update.responseStatusCode == 200) { if (update.responseStatusCode == 200) {
@ -454,7 +456,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
failed = true; failed = true;
} }
Log.error( Log.error(
"Got error while uploading: ${update.responseStatusCode}", 'Got error while uploading: ${update.responseStatusCode}',
); );
} }
} }
@ -463,7 +465,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
for (final messageId in media.messageIds!) { for (final messageId in media.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
messageId, messageId,
MessagesCompanion( const MessagesCompanion(
acknowledgeByServer: Value(true), acknowledgeByServer: Value(true),
errorWhileSending: Value(true), errorWhileSending: Value(true),
), ),
@ -474,13 +476,13 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
'Status update for ${update.task.taskId} with status ${update.status}'); 'Status update for ${update.task.taskId} with status ${update.status}');
} }
Future handleUploadSuccess(MediaUpload media) async { Future<void> handleUploadSuccess(MediaUpload media) async {
Log.info("Upload of ${media.mediaUploadId} success!"); Log.info('Upload of ${media.mediaUploadId} success!');
currentUploadTasks.remove(media.mediaUploadId); currentUploadTasks.remove(media.mediaUploadId);
await twonlyDB.mediaUploadsDao.updateMediaUpload( await twonlyDB.mediaUploadsDao.updateMediaUpload(
media.mediaUploadId, media.mediaUploadId,
MediaUploadsCompanion( const MediaUploadsCompanion(
state: Value(UploadState.receiverNotified), state: Value(UploadState.receiverNotified),
), ),
); );
@ -488,7 +490,7 @@ Future handleUploadSuccess(MediaUpload media) async {
for (final messageId in media.messageIds!) { for (final messageId in media.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
messageId, messageId,
MessagesCompanion( const MessagesCompanion(
acknowledgeByServer: Value(true), acknowledgeByServer: Value(true),
errorWhileSending: Value(false), errorWhileSending: Value(false),
), ),
@ -496,13 +498,13 @@ Future handleUploadSuccess(MediaUpload media) async {
} }
} }
Future handleUploadError(MediaUpload mediaUpload) async { Future<void> handleUploadError(MediaUpload mediaUpload) async {
// if the messageIds are already there notify the user about this error... // if the messageIds are already there notify the user about this error...
if (mediaUpload.messageIds != null) { if (mediaUpload.messageIds != null) {
for (int messageId in mediaUpload.messageIds!) { for (final messageId in mediaUpload.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
messageId, messageId,
MessagesCompanion( const MessagesCompanion(
errorWhileSending: Value(true), errorWhileSending: Value(true),
), ),
); );
@ -511,20 +513,20 @@ Future handleUploadError(MediaUpload mediaUpload) async {
await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId); await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId);
} }
Future handleMediaUpload(MediaUpload media) async { Future<void> handleMediaUpload(MediaUpload media) async {
Uint8List bytesToUpload = final bytesToUpload =
await readSendMediaFile(media.mediaUploadId, "encrypted"); await readSendMediaFile(media.mediaUploadId, 'encrypted');
if (media.messageIds == null) return; if (media.messageIds == null) return;
List<int> messageIds = media.messageIds!; final messageIds = media.messageIds!;
List<Uint8List> downloadTokens = []; final downloadTokens = <Uint8List>[];
List<TextMessage> messagesOnSuccess = []; final messagesOnSuccess = <TextMessage>[];
for (var i = 0; i < messageIds.length; i++) { for (var i = 0; i < messageIds.length; i++) {
Message? message = await twonlyDB.messagesDao final message = await twonlyDB.messagesDao
.getMessageByMessageId(messageIds[i]) .getMessageByMessageId(messageIds[i])
.getSingleOrNull(); .getSingleOrNull();
if (message == null) continue; if (message == null) continue;
@ -536,7 +538,7 @@ Future handleMediaUpload(MediaUpload media) async {
final downloadToken = createDownloadToken(); final downloadToken = createDownloadToken();
MessageJson msg = MessageJson( final msg = MessageJson(
kind: MessageKind.media, kind: MessageKind.media,
messageSenderId: messageIds[i], messageSenderId: messageIds[i],
content: MediaMessageContent( content: MediaMessageContent(
@ -552,19 +554,19 @@ Future handleMediaUpload(MediaUpload media) async {
timestamp: media.metadata!.messageSendAt, timestamp: media.metadata!.messageSendAt,
); );
Uint8List plaintextContent = final plaintextContent =
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))); Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
Contact? contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
.getContactByUserId(message.contactId) .getContactByUserId(message.contactId)
.getSingleOrNull(); .getSingleOrNull();
if (contact == null || contact.deleted) { if (contact == null || contact.deleted) {
Log.warn( Log.warn(
"Contact deleted ${message.contactId} or not found in database."); 'Contact deleted ${message.contactId} or not found in database.');
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId, message.messageId,
MessagesCompanion(errorWhileSending: Value(true)), const MessagesCompanion(errorWhileSending: Value(true)),
); );
continue; continue;
} }
@ -575,14 +577,14 @@ Future handleMediaUpload(MediaUpload media) async {
message.sendAt, message.sendAt,
); );
Uint8List? encryptedBytes = await signalEncryptMessage( final encryptedBytes = await signalEncryptMessage(
message.contactId, message.contactId,
plaintextContent, plaintextContent,
); );
if (encryptedBytes == null) continue; if (encryptedBytes == null) continue;
var messageOnSuccess = TextMessage() final messageOnSuccess = TextMessage()
..body = encryptedBytes ..body = encryptedBytes
..userId = Int64(message.contactId); ..userId = Int64(message.contactId);
@ -615,29 +617,29 @@ Future handleMediaUpload(MediaUpload media) async {
final uploadRequestBytes = uploadRequest.writeToBuffer(); final uploadRequestBytes = uploadRequest.writeToBuffer();
String? apiAuthTokenRaw = final apiAuthTokenRaw = await const FlutterSecureStorage()
await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken); .read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthTokenRaw == null) { if (apiAuthTokenRaw == null) {
Log.error("api auth token not defined."); Log.error('api auth token not defined.');
return; return;
} }
String apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
File uploadRequestFile = await writeSendMediaFile( final uploadRequestFile = await writeSendMediaFile(
media.mediaUploadId, media.mediaUploadId,
"upload", 'upload',
uploadRequestBytes, uploadRequestBytes,
); );
String apiUrl = final apiUrl =
"http${apiService.apiSecure}://${apiService.apiHost}/api/upload"; 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
try { try {
Log.info("Starting upload from ${media.mediaUploadId}"); Log.info('Starting upload from ${media.mediaUploadId}');
final task = UploadTask.fromFile( final task = UploadTask.fromFile(
taskId: "upload_${media.mediaUploadId}", taskId: 'upload_${media.mediaUploadId}',
displayName: (media.metadata?.isVideo ?? false) ? "image" : "video", displayName: (media.metadata?.isVideo ?? false) ? 'image' : 'video',
file: uploadRequestFile, file: uploadRequestFile,
url: apiUrl, url: apiUrl,
priority: 0, priority: 0,
@ -652,62 +654,62 @@ Future handleMediaUpload(MediaUpload media) async {
try { try {
await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken); await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken);
} catch (e) { } catch (e) {
Log.error("Fast upload failed: $e. Using slow method directly."); Log.error('Fast upload failed: $e. Using slow method directly.');
enqueueUploadTask(media.mediaUploadId); await enqueueUploadTask(media.mediaUploadId);
} }
} catch (e) { } catch (e) {
Log.error("Exception during upload: $e"); Log.error('Exception during upload: $e');
} }
} }
Map<int, UploadTask> currentUploadTasks = {}; Map<int, UploadTask> currentUploadTasks = {};
Future enqueueUploadTask(int mediaUploadId) async { Future<void> enqueueUploadTask(int mediaUploadId) async {
if (currentUploadTasks[mediaUploadId] == null) { if (currentUploadTasks[mediaUploadId] == null) {
Log.info("could not enqueue upload task: $mediaUploadId"); Log.info('could not enqueue upload task: $mediaUploadId');
return; return;
} }
Log.info("Enqueue upload task: $mediaUploadId"); Log.info('Enqueue upload task: $mediaUploadId');
await FileDownloader().enqueue(currentUploadTasks[mediaUploadId]!); await FileDownloader().enqueue(currentUploadTasks[mediaUploadId]!);
currentUploadTasks.remove(mediaUploadId); currentUploadTasks.remove(mediaUploadId);
await twonlyDB.mediaUploadsDao.updateMediaUpload( await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId, mediaUploadId,
MediaUploadsCompanion( const MediaUploadsCompanion(
state: Value(UploadState.uploadTaskStarted), state: Value(UploadState.uploadTaskStarted),
), ),
); );
} }
Future handleUploadWhenAppGoesBackground() async { Future<void> handleUploadWhenAppGoesBackground() async {
if (currentUploadTasks.keys.isEmpty) { if (currentUploadTasks.keys.isEmpty) {
return; return;
} }
Log.info("App goes into background. Enqueue uploads to the background."); Log.info('App goes into background. Enqueue uploads to the background.');
final keys = currentUploadTasks.keys.toList(); final keys = currentUploadTasks.keys.toList();
for (final key in keys) { for (final key in keys) {
enqueueUploadTask(key); enqueueUploadTask(key);
} }
} }
Future uploadFileFast( Future<void> uploadFileFast(
MediaUpload media, MediaUpload media,
Uint8List uploadRequestFile, Uint8List uploadRequestFile,
String apiUrl, String apiUrl,
String apiAuthToken, String apiAuthToken,
) async { ) async {
var requestMultipart = http.MultipartRequest( final requestMultipart = http.MultipartRequest(
"POST", 'POST',
Uri.parse(apiUrl), Uri.parse(apiUrl),
); );
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken; requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
requestMultipart.files.add(http.MultipartFile.fromBytes( requestMultipart.files.add(http.MultipartFile.fromBytes(
"file", 'file',
uploadRequestFile, uploadRequestFile,
filename: "upload", filename: 'upload',
)); ));
final response = await requestMultipart.send(); final response = await requestMultipart.send();
@ -721,9 +723,9 @@ Future uploadFileFast(
} }
Future<bool> compressVideoIfExists(int mediaUploadId) async { Future<bool> compressVideoIfExists(int mediaUploadId) async {
String basePath = await getMediaFilePath(mediaUploadId, "send"); final basePath = await getMediaFilePath(mediaUploadId, 'send');
File videoOriginalFile = File("$basePath.original.mp4"); final videoOriginalFile = File('$basePath.original.mp4');
File videoCompressedFile = File("$basePath.mp4"); final videoCompressedFile = File('$basePath.mp4');
if (videoCompressedFile.existsSync()) { if (videoCompressedFile.existsSync()) {
// file is already compressed and exists // file is already compressed and exists
@ -735,38 +737,35 @@ Future<bool> compressVideoIfExists(int mediaUploadId) async {
return false; return false;
} }
Stopwatch stopwatch = Stopwatch(); final stopwatch = Stopwatch()..start();
stopwatch.start();
MediaInfo? mediaInfo; MediaInfo? mediaInfo;
try { try {
mediaInfo = await VideoCompress.compressVideo( mediaInfo = await VideoCompress.compressVideo(
videoOriginalFile.path, videoOriginalFile.path,
quality: VideoQuality.Res1280x720Quality, quality: VideoQuality.Res1280x720Quality,
deleteOrigin: false,
includeAudio: includeAudio:
true, // https://github.com/jonataslaw/VideoCompress/issues/184 true, // https://github.com/jonataslaw/VideoCompress/issues/184
); );
Log.info("Video has now size of ${mediaInfo!.filesize} bytes."); Log.info('Video has now size of ${mediaInfo!.filesize} bytes.');
if (mediaInfo.filesize! >= 30 * 1000 * 1000) { if (mediaInfo.filesize! >= 30 * 1000 * 1000) {
// if the media file is over 20MB compress it with low quality // if the media file is over 20MB compress it with low quality
mediaInfo = await VideoCompress.compressVideo( mediaInfo = await VideoCompress.compressVideo(
videoOriginalFile.path, videoOriginalFile.path,
quality: VideoQuality.Res960x540Quality, quality: VideoQuality.Res960x540Quality,
deleteOrigin: false,
includeAudio: true, includeAudio: true,
); );
} }
} catch (e) { } catch (e) {
Log.error("during video compression: $e"); Log.error('during video compression: $e');
} }
stopwatch.stop(); stopwatch.stop();
Log.info("It took ${stopwatch.elapsedMilliseconds}ms to compress the video"); Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video');
if (mediaInfo == null) { if (mediaInfo == null) {
Log.error("could not compress video."); Log.error('could not compress video.');
// as a fall back use the non compressed version // as a fall back use the non compressed version
await videoOriginalFile.copy(videoCompressedFile.path); await videoOriginalFile.copy(videoCompressedFile.path);
await videoOriginalFile.delete(); await videoOriginalFile.delete();
@ -780,26 +779,26 @@ Future<bool> compressVideoIfExists(int mediaUploadId) async {
/// --- helper functions --- /// --- helper functions ---
Future<Uint8List> readSendMediaFile(int mediaUploadId, String type) async { Future<Uint8List> readSendMediaFile(int mediaUploadId, String type) async {
String basePath = await getMediaFilePath(mediaUploadId, "send"); final basePath = await getMediaFilePath(mediaUploadId, 'send');
File file = File("$basePath.$type"); final file = File('$basePath.$type');
if (!await file.exists()) { if (!file.existsSync()) {
throw Exception("$file not found"); throw Exception('$file not found');
} }
return await file.readAsBytes(); return file.readAsBytes();
} }
Future<File> writeSendMediaFile( Future<File> writeSendMediaFile(
int mediaUploadId, String type, Uint8List data) async { int mediaUploadId, String type, Uint8List data) async {
String basePath = await getMediaFilePath(mediaUploadId, "send"); final basePath = await getMediaFilePath(mediaUploadId, 'send');
File file = File("$basePath.$type"); final file = File('$basePath.$type');
await file.writeAsBytes(data); await file.writeAsBytes(data);
return file; return file;
} }
Future<void> deleteSendMediaFile(int mediaUploadId, String type) async { Future<void> deleteSendMediaFile(int mediaUploadId, String type) async {
String basePath = await getMediaFilePath(mediaUploadId, "send"); final basePath = await getMediaFilePath(mediaUploadId, 'send');
File file = File("$basePath.$type"); final file = File('$basePath.$type');
if (await file.exists()) { if (file.existsSync()) {
await file.delete(); await file.delete();
} }
} }
@ -807,7 +806,7 @@ Future<void> deleteSendMediaFile(int mediaUploadId, String type) async {
Future<String> getMediaFilePath(dynamic mediaId, String type) async { Future<String> getMediaFilePath(dynamic mediaId, String type) async {
final basedir = await getApplicationSupportDirectory(); final basedir = await getApplicationSupportDirectory();
final mediaSendDir = Directory(join(basedir.path, 'media', type)); final mediaSendDir = Directory(join(basedir.path, 'media', type));
if (!await mediaSendDir.exists()) { if (!mediaSendDir.existsSync()) {
await mediaSendDir.create(recursive: true); await mediaSendDir.create(recursive: true);
} }
return join(mediaSendDir.path, '$mediaId'); return join(mediaSendDir.path, '$mediaId');
@ -816,7 +815,7 @@ Future<String> getMediaFilePath(dynamic mediaId, String type) async {
Future<String> getMediaBaseFilePath(String type) async { Future<String> getMediaBaseFilePath(String type) async {
final basedir = await getApplicationSupportDirectory(); final basedir = await getApplicationSupportDirectory();
final mediaSendDir = Directory(join(basedir.path, 'media', type)); final mediaSendDir = Directory(join(basedir.path, 'media', type));
if (!await mediaSendDir.exists()) { if (!mediaSendDir.existsSync()) {
await mediaSendDir.create(recursive: true); await mediaSendDir.create(recursive: true);
} }
return mediaSendDir.path; return mediaSendDir.path;
@ -825,18 +824,15 @@ Future<String> getMediaBaseFilePath(String type) async {
/// combines two utf8 list /// combines two utf8 list
Uint8List combineUint8Lists(Uint8List list1, Uint8List list2) { Uint8List combineUint8Lists(Uint8List list1, Uint8List list2) {
final combinedLength = 4 + list1.length + list2.length; final combinedLength = 4 + list1.length + list2.length;
final combinedList = Uint8List(combinedLength); final combinedList = Uint8List(combinedLength)
final byteData = ByteData.sublistView(combinedList); ..setRange(4, 4 + list1.length, list1)
byteData.setInt32( ..setRange(4 + list1.length, combinedLength, list2);
0, list1.length, Endian.big); // Store size in big-endian format
combinedList.setRange(4, 4 + list1.length, list1);
combinedList.setRange(4 + list1.length, combinedLength, list2);
return combinedList; return combinedList;
} }
List<Uint8List> extractUint8Lists(Uint8List combinedList) { List<Uint8List> extractUint8Lists(Uint8List combinedList) {
final byteData = ByteData.sublistView(combinedList); final byteData = ByteData.sublistView(combinedList);
final sizeOfList1 = byteData.getInt32(0, Endian.big); final sizeOfList1 = byteData.getInt32(0);
final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1); final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1);
final list2 = Uint8List.view(combinedList.buffer, 4 + sizeOfList1, final list2 = Uint8List.view(combinedList.buffer, 4 + sizeOfList1,
combinedList.lengthInBytes - 4 - sizeOfList1); combinedList.lengthInBytes - 4 - sizeOfList1);
@ -845,7 +841,7 @@ List<Uint8List> extractUint8Lists(Uint8List combinedList) {
Future<void> purgeSendMediaFiles() async { Future<void> purgeSendMediaFiles() async {
final basedir = await getApplicationSupportDirectory(); final basedir = await getApplicationSupportDirectory();
final directory = Directory(join(basedir.path, 'media', "send")); final directory = Directory(join(basedir.path, 'media', 'send'));
await purgeMediaFiles(directory); await purgeMediaFiles(directory);
} }
@ -858,10 +854,10 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList(List<int>.generate(
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16))); (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)));
Uint8List createDownloadToken() { Uint8List createDownloadToken() {
final Random random = Random(); final random = Random();
Uint8List token = Uint8List(32); final token = Uint8List(32);
for (int j = 0; j < 32; j++) { for (var j = 0; j < 32; j++) {
token[j] = random.nextInt(256); // Generate a random byte (0-255) token[j] = random.nextInt(256); // Generate a random byte (0-255)
} }
return token; return token;

View file

@ -1,20 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/services/api/server_messages.dart' import 'package:twonly/src/services/api/server_messages.dart'
show messageGetsAck; show messageGetsAck;
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -22,12 +21,12 @@ import 'package:twonly/src/utils/storage.dart';
final lockRetransmission = Mutex(); final lockRetransmission = Mutex();
Future tryTransmitMessages() async { Future<void> tryTransmitMessages() async {
return await lockRetransmission.protect(() async { return lockRetransmission.protect(() async {
final retransIds = final retransIds =
await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages(); await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
Log.info("Retransmitting ${retransIds.length} text messages"); Log.info('Retransmitting ${retransIds.length} text messages');
if (retransIds.isEmpty) return; if (retransIds.isEmpty) return;
@ -37,53 +36,51 @@ Future tryTransmitMessages() async {
}); });
} }
Future sendRetransmitMessage(int retransId) async { Future<void> sendRetransmitMessage(int retransId) async {
try { try {
MessageRetransmission? retrans = await twonlyDB.messageRetransmissionDao final retrans = await twonlyDB.messageRetransmissionDao
.getRetransmissionById(retransId) .getRetransmissionById(retransId)
.getSingleOrNull(); .getSingleOrNull();
if (retrans == null) { if (retrans == null) {
Log.error("$retransId not found in database"); Log.error('$retransId not found in database');
return; return;
} }
if (retrans.acknowledgeByServerAt != null) { if (retrans.acknowledgeByServerAt != null) {
Log.error("$retransId message already retransmitted"); Log.error('$retransId message already retransmitted');
return; return;
} }
MessageJson json = MessageJson.fromJson( final json = MessageJson.fromJson(jsonDecode(
jsonDecode(
utf8.decode( utf8.decode(
gzip.decode(retrans.plaintextContent), gzip.decode(retrans.plaintextContent),
), ),
), ) as Map<String, dynamic>);
);
Log.info("Retransmitting $retransId: ${json.kind} to ${retrans.contactId}"); Log.info('Retransmitting $retransId: ${json.kind} to ${retrans.contactId}');
Contact? contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
.getContactByUserId(retrans.contactId) .getContactByUserId(retrans.contactId)
.getSingleOrNull(); .getSingleOrNull();
if (contact == null || contact.deleted) { if (contact == null || contact.deleted) {
Log.warn("Contact deleted $retransId or not found in database."); Log.warn('Contact deleted $retransId or not found in database.');
if (retrans.messageId != null) { if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!, retrans.messageId!,
MessagesCompanion(errorWhileSending: Value(true)), const MessagesCompanion(errorWhileSending: Value(true)),
); );
} }
return; return;
} }
Uint8List? encryptedBytes = await signalEncryptMessage( final encryptedBytes = await signalEncryptMessage(
retrans.contactId, retrans.contactId,
retrans.plaintextContent, retrans.plaintextContent,
); );
if (encryptedBytes == null) { if (encryptedBytes == null) {
Log.error("Could not encrypt the message. Aborting and trying again."); Log.error('Could not encrypt the message. Aborting and trying again.');
return; return;
} }
@ -96,27 +93,27 @@ Future sendRetransmitMessage(int retransId) async {
), ),
); );
Result resp = await apiService.sendTextMessage( final resp = await apiService.sendTextMessage(
retrans.contactId, retrans.contactId,
encryptedBytes, encryptedBytes,
retrans.pushData, retrans.pushData,
); );
bool retry = true; var retry = true;
if (resp.isError) { if (resp.isError) {
Log.error("Could not retransmit message."); Log.error('Could not retransmit message.');
if (resp.error == ErrorCode.UserIdNotFound) { if (resp.error == ErrorCode.UserIdNotFound) {
retry = false; retry = false;
if (retrans.messageId != null) { if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!, retrans.messageId!,
MessagesCompanion(errorWhileSending: Value(true)), const MessagesCompanion(errorWhileSending: Value(true)),
); );
} }
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
retrans.contactId, retrans.contactId,
ContactsCompanion(deleted: Value(true)), const ContactsCompanion(deleted: Value(true)),
); );
} }
} }
@ -126,7 +123,7 @@ Future sendRetransmitMessage(int retransId) async {
if (retrans.messageId != null) { if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!, retrans.messageId!,
MessagesCompanion( const MessagesCompanion(
acknowledgeByServer: Value(true), acknowledgeByServer: Value(true),
errorWhileSending: Value(false), errorWhileSending: Value(false),
), ),
@ -148,13 +145,13 @@ Future sendRetransmitMessage(int retransId) async {
} }
} }
} catch (e) { } catch (e) {
Log.error("error resending message: $e"); Log.error('error resending message: $e');
await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId); await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId);
} }
} }
// encrypts and stores the message and then sends it in the background // encrypts and stores the message and then sends it in the background
Future encryptAndSendMessageAsync( Future<void> encryptAndSendMessageAsync(
int? messageId, int? messageId,
int userId, int userId,
MessageJson msg, { MessageJson msg, {
@ -169,7 +166,8 @@ Future encryptAndSendMessageAsync(
pushData = await getPushData(userId, pushNotification); pushData = await getPushData(userId, pushNotification);
} }
int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission( final retransId =
await twonlyDB.messageRetransmissionDao.insertRetransmission(
MessageRetransmissionsCompanion( MessageRetransmissionsCompanion(
contactId: Value(userId), contactId: Value(userId),
messageId: Value(messageId), messageId: Value(messageId),
@ -179,13 +177,13 @@ Future encryptAndSendMessageAsync(
); );
if (retransId == null) { if (retransId == null) {
Log.error("Could not insert the message into the retransmission database"); Log.error('Could not insert the message into the retransmission database');
return; return;
} }
msg.retransId = retransId; msg.retransId = retransId;
Uint8List plaintextContent = final plaintextContent =
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))); Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
await twonlyDB.messageRetransmissionDao.updateRetransmission( await twonlyDB.messageRetransmissionDao.updateRetransmission(
@ -194,29 +192,29 @@ Future encryptAndSendMessageAsync(
plaintextContent: Value(plaintextContent))); plaintextContent: Value(plaintextContent)));
// this can now be done in the background... // this can now be done in the background...
sendRetransmitMessage(retransId); unawaited(sendRetransmitMessage(retransId));
} }
Future sendTextMessage( Future<void> sendTextMessage(
int target, int target,
TextMessageContent content, TextMessageContent content,
PushNotification? pushNotification, PushNotification? pushNotification,
) async { ) async {
DateTime messageSendAt = DateTime.now(); final messageSendAt = DateTime.now();
DateTime? openedAt; DateTime? openedAt;
if (pushNotification != null && pushNotification.hasReactionContent()) { if (pushNotification != null && pushNotification.hasReactionContent()) {
openedAt = DateTime.now(); openedAt = DateTime.now();
} }
int? messageId = await twonlyDB.messagesDao.insertMessage( final messageId = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
contactId: Value(target), contactId: Value(target),
kind: Value(MessageKind.textMessage), kind: const Value(MessageKind.textMessage),
sendAt: Value(messageSendAt), sendAt: Value(messageSendAt),
responseToOtherMessageId: Value(content.responseToMessageId), responseToOtherMessageId: Value(content.responseToMessageId),
responseToMessageId: Value(content.responseToOtherMessageId), responseToMessageId: Value(content.responseToOtherMessageId),
downloadState: Value(DownloadState.downloaded), downloadState: const Value(DownloadState.downloaded),
openedAt: Value(openedAt), openedAt: Value(openedAt),
contentJson: Value( contentJson: Value(
jsonEncode(content.toJson()), jsonEncode(content.toJson()),
@ -230,7 +228,7 @@ Future sendTextMessage(
pushNotification.messageId = Int64(messageId); pushNotification.messageId = Int64(messageId);
} }
MessageJson msg = MessageJson( final msg = MessageJson(
kind: MessageKind.textMessage, kind: MessageKind.textMessage,
messageSenderId: messageId, messageSenderId: messageId,
content: content, content: content,
@ -245,11 +243,11 @@ Future sendTextMessage(
); );
} }
Future notifyContactAboutOpeningMessage( Future<void> notifyContactAboutOpeningMessage(
int fromUserId, int fromUserId,
List<int> messageOtherIds, List<int> messageOtherIds,
) async { ) async {
int biggestMessageId = messageOtherIds.first; var biggestMessageId = messageOtherIds.first;
for (final messageOtherId in messageOtherIds) { for (final messageOtherId in messageOtherIds) {
if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId; if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId;
@ -267,17 +265,16 @@ Future notifyContactAboutOpeningMessage(
await updateLastMessageId(fromUserId, biggestMessageId); await updateLastMessageId(fromUserId, biggestMessageId);
} }
Future notifyContactsAboutProfileChange() async { Future<void> notifyContactsAboutProfileChange() async {
List<Contact> contacts = final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
await twonlyDB.contactsDao.getAllNotBlockedContacts();
UserData? user = await getUser(); final user = await getUser();
if (user == null) return; if (user == null) return;
if (user.avatarSvg == null) return; if (user.avatarSvg == null) return;
for (Contact contact in contacts) { for (final contact in contacts) {
if (contact.myAvatarCounter < user.avatarCounter) { if (contact.myAvatarCounter < user.avatarCounter) {
twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
contact.userId, contact.userId,
ContactsCompanion( ContactsCompanion(
myAvatarCounter: Value(user.avatarCounter), myAvatarCounter: Value(user.avatarCounter),

View file

@ -1,14 +1,15 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/media_uploads_table.dart'; import 'package:twonly/src/database/tables/media_uploads_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
as client; as client;
@ -16,10 +17,10 @@ import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server; as server;
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
@ -31,7 +32,7 @@ import 'package:twonly/src/views/components/animate_icon.dart';
final lockHandleServerMessage = Mutex(); final lockHandleServerMessage = Mutex();
Future handleServerMessage(server.ServerToClient msg) async { Future<void> handleServerMessage(server.ServerToClient msg) async {
return lockHandleServerMessage.protect(() async { return lockHandleServerMessage.protect(() async {
client.Response? response; client.Response? response;
@ -39,34 +40,35 @@ Future handleServerMessage(server.ServerToClient msg) async {
if (msg.v0.hasRequestNewPreKeys()) { if (msg.v0.hasRequestNewPreKeys()) {
response = await handleRequestNewPreKey(); response = await handleRequestNewPreKey();
} else if (msg.v0.hasNewMessage()) { } else if (msg.v0.hasNewMessage()) {
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body); final body = Uint8List.fromList(msg.v0.newMessage.body);
int fromUserId = msg.v0.newMessage.fromUserId.toInt(); final fromUserId = msg.v0.newMessage.fromUserId.toInt();
response = await handleNewMessage(fromUserId, body); response = await handleNewMessage(fromUserId, body);
} else { } else {
Log.error("Got a new message from the server: $msg"); Log.error('Got a new message from the server: $msg');
response = client.Response()..error = ErrorCode.InternalError; response = client.Response()..error = ErrorCode.InternalError;
} }
} catch (e) { } catch (e) {
response = client.Response()..error = ErrorCode.InternalError; response = client.Response()..error = ErrorCode.InternalError;
} }
var v0 = client.V0() final v0 = client.V0()
..seq = msg.v0.seq ..seq = msg.v0.seq
..response = response; ..response = response;
apiService.sendResponse(ClientToServer()..v0 = v0); await apiService.sendResponse(ClientToServer()..v0 = v0);
}); });
} }
DateTime lastSignalDecryptMessage = DateTime.now().subtract(Duration(hours: 1)); DateTime lastSignalDecryptMessage =
DateTime lastPushKeyRequest = DateTime.now().subtract(Duration(hours: 1)); DateTime.now().subtract(const Duration(hours: 1));
DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
bool messageGetsAck(MessageKind kind) { bool messageGetsAck(MessageKind kind) {
return kind != MessageKind.pushKey && kind != MessageKind.ack; return kind != MessageKind.pushKey && kind != MessageKind.ack;
} }
Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async { Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
MessageJson? message = await signalDecryptMessage(fromUserId, body); final message = await signalDecryptMessage(fromUserId, body);
if (message == null) { if (message == null) {
final encryptedHash = (await Sha256().hash(body)).bytes; final encryptedHash = (await Sha256().hash(body)).bytes;
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
@ -79,17 +81,17 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
), ),
); );
Log.error("Could not decrypt others message!"); Log.error('Could not decrypt others message!');
// Message is not valid, so server can delete it // Message is not valid, so server can delete it
var ok = client.Response_Ok()..none = true; final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
Log.info("Got: ${message.kind} from $fromUserId"); Log.info('Got: ${message.kind} from $fromUserId');
if (messageGetsAck(message.kind) && message.retransId != null) { if (messageGetsAck(message.kind) && message.retransId != null) {
Log.info("Sending ACK for ${message.kind}"); Log.info('Sending ACK for ${message.kind}');
/// ACK every message /// ACK every message
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
@ -111,7 +113,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
final content = message.content; final content = message.content;
if (content is AckContent) { if (content is AckContent) {
if (content.messageIdToAck != null) { if (content.messageIdToAck != null) {
final update = MessagesCompanion( const update = MessagesCompanion(
acknowledgeByUser: Value(true), acknowledgeByUser: Value(true),
errorWhileSending: Value(false), errorWhileSending: Value(false),
); );
@ -125,10 +127,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
await twonlyDB.messageRetransmissionDao await twonlyDB.messageRetransmissionDao
.deleteRetransmissionById(content.retransIdToAck); .deleteRetransmissionById(content.retransIdToAck);
} }
break;
case MessageKind.signalDecryptError: case MessageKind.signalDecryptError:
Log.error( Log.error(
"Got signal decrypt error from other user! Sending all non ACK messages again."); 'Got signal decrypt error from other user! Sending all non ACK messages again.');
final content = message.content; final content = message.content;
if (content is SignalDecryptErrorContent) { if (content is SignalDecryptErrorContent) {
@ -140,16 +141,15 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
final message = await twonlyDB.messageRetransmissionDao final message = await twonlyDB.messageRetransmissionDao
.getRetransmissionFromHash(fromUserId, hash); .getRetransmissionFromHash(fromUserId, hash);
if (message != null) { if (message != null) {
sendRetransmitMessage(message.retransmissionId); unawaited(sendRetransmitMessage(message.retransmissionId));
} }
} }
break;
case MessageKind.contactRequest: case MessageKind.contactRequest:
return handleContactRequest(fromUserId, message); return handleContactRequest(fromUserId, message);
case MessageKind.flameSync: case MessageKind.flameSync:
Contact? contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();
if (contact != null && contact.lastFlameCounterChange != null) { if (contact != null && contact.lastFlameCounterChange != null) {
@ -188,12 +188,12 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
openedMessage.mediaRetransmissionState == openedMessage.mediaRetransmissionState ==
MediaRetransmitting.none && MediaRetransmitting.none &&
openedMessage.sendAt openedMessage.sendAt
.isAfter(DateTime.now().subtract(Duration(days: 2)))) { .isAfter(DateTime.now().subtract(const Duration(days: 2)))) {
// reset the media upload state to pending, // reset the media upload state to pending,
// this will cause the media to be re-encrypted again // this will cause the media to be re-encrypted again
twonlyDB.mediaUploadsDao.updateMediaUpload( await twonlyDB.mediaUploadsDao.updateMediaUpload(
openedMessage.mediaUploadId!, openedMessage.mediaUploadId!,
MediaUploadsCompanion( const MediaUploadsCompanion(
state: Value( state: Value(
UploadState.pending, UploadState.pending,
), ),
@ -203,18 +203,18 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
await twonlyDB.messagesDao.updateMessageByOtherUser( await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId, fromUserId,
message.messageReceiverId!, message.messageReceiverId!,
MessagesCompanion( const MessagesCompanion(
downloadState: Value(DownloadState.pending), downloadState: Value(DownloadState.pending),
mediaRetransmissionState: mediaRetransmissionState:
Value(MediaRetransmitting.retransmitted), Value(MediaRetransmitting.retransmitted),
), ),
); );
retryMediaUpload(false); unawaited(retryMediaUpload(false));
} else { } else {
await twonlyDB.messagesDao.updateMessageByOtherUser( await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId, fromUserId,
message.messageReceiverId!, message.messageReceiverId!,
MessagesCompanion( const MessagesCompanion(
errorWhileSending: Value(true), errorWhileSending: Value(true),
), ),
); );
@ -226,7 +226,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
if (message.messageReceiverId != null) { if (message.messageReceiverId != null) {
final update = MessagesCompanion( final update = MessagesCompanion(
openedAt: Value(message.timestamp), openedAt: Value(message.timestamp),
errorWhileSending: Value(false), errorWhileSending: const Value(false),
); );
await twonlyDB.messagesDao.updateMessageByOtherUser( await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId, fromUserId,
@ -243,20 +243,17 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
); );
} }
} }
break;
case MessageKind.rejectRequest: case MessageKind.rejectRequest:
await deleteContact(fromUserId); await deleteContact(fromUserId);
break;
case MessageKind.acceptRequest: case MessageKind.acceptRequest:
final update = ContactsCompanion(accepted: Value(true)); const update = ContactsCompanion(accepted: Value(true));
await twonlyDB.contactsDao.updateContact(fromUserId, update); await twonlyDB.contactsDao.updateContact(fromUserId, update);
notifyContactsAboutProfileChange(); unawaited(notifyContactsAboutProfileChange());
break;
case MessageKind.profileChange: case MessageKind.profileChange:
var content = message.content; final content = message.content;
if (content is ProfileContent) { if (content is ProfileContent) {
final update = ContactsCompanion( final update = ContactsCompanion(
avatarSvg: Value(content.avatarSvg), avatarSvg: Value(content.avatarSvg),
@ -264,14 +261,13 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
); );
await twonlyDB.contactsDao.updateContact(fromUserId, update); await twonlyDB.contactsDao.updateContact(fromUserId, update);
} }
createPushAvatars(); unawaited(createPushAvatars());
break;
case MessageKind.requestPushKey: case MessageKind.requestPushKey:
if (lastPushKeyRequest if (lastPushKeyRequest
.isBefore(DateTime.now().subtract(Duration(seconds: 60)))) { .isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) {
lastPushKeyRequest = DateTime.now(); lastPushKeyRequest = DateTime.now();
setupNotificationWithUsers(forceContact: fromUserId); unawaited(setupNotificationWithUsers(forceContact: fromUserId));
} }
case MessageKind.pushKey: case MessageKind.pushKey:
@ -282,14 +278,15 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
} }
} }
// ignore: no_default_cases
default: default:
if (message.kind != MessageKind.textMessage && if (message.kind != MessageKind.textMessage &&
message.kind != MessageKind.media && message.kind != MessageKind.media &&
message.kind != MessageKind.storedMediaFile && message.kind != MessageKind.storedMediaFile &&
message.kind != MessageKind.reopenedMedia) { message.kind != MessageKind.reopenedMedia) {
Log.error("Got unknown MessageKind $message"); Log.error('Got unknown MessageKind $message');
} else if (message.messageSenderId == null) { } else if (message.messageSenderId == null) {
Log.error("Messageid not defined $message"); Log.error('Messageid not defined $message');
} else { } else {
if (message.kind == MessageKind.storedMediaFile) { if (message.kind == MessageKind.storedMediaFile) {
if (message.messageReceiverId != null) { if (message.messageReceiverId != null) {
@ -297,7 +294,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
await twonlyDB.messagesDao.updateMessageByOtherUser( await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId, fromUserId,
message.messageReceiverId!, message.messageReceiverId!,
MessagesCompanion( const MessagesCompanion(
mediaStored: Value(true), mediaStored: Value(true),
errorWhileSending: Value(false), errorWhileSending: Value(false),
), ),
@ -308,11 +305,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
.getSingleOrNull(); .getSingleOrNull();
if (msg != null && msg.mediaUploadId != null) { if (msg != null && msg.mediaUploadId != null) {
final filePath = final filePath =
await getMediaFilePath(msg.mediaUploadId, "send"); await getMediaFilePath(msg.mediaUploadId, 'send');
if (filePath.contains("mp4")) { if (filePath.contains('mp4')) {
createThumbnailsForVideo(File(filePath)); unawaited(createThumbnailsForVideo(File(filePath)));
} else { } else {
createThumbnailsForImage(File(filePath)); unawaited(createThumbnailsForImage(File(filePath)));
} }
} }
} }
@ -330,8 +327,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
.deleteMessagesByMessageId(openedMessage.messageId); .deleteMessagesByMessageId(openedMessage.messageId);
} else { } else {
Log.error( Log.error(
"Got a duplicated message from other user: ${message.messageSenderId!}"); 'Got a duplicated message from other user: ${message.messageSenderId!}');
var ok = client.Response_Ok()..none = true; final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
} }
@ -340,7 +337,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
int? responseToOtherMessageId; int? responseToOtherMessageId;
int? messageId; int? messageId;
bool acknowledgeByUser = false; var acknowledgeByUser = false;
DateTime? openedAt; DateTime? openedAt;
if (message.kind == MessageKind.reopenedMedia) { if (message.kind == MessageKind.reopenedMedia) {
@ -369,7 +366,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
fromUserId, fromUserId,
responseToMessageId, responseToMessageId,
MessagesCompanion( MessagesCompanion(
errorWhileSending: Value(false), errorWhileSending: const Value(false),
openedAt: Value( openedAt: Value(
DateTime.now(), DateTime.now(),
), // when a user reacted to the media file, it should be marked as opened ), // when a user reacted to the media file, it should be marked as opened
@ -377,13 +374,13 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
); );
} }
String contentJson = jsonEncode(content.toJson()); final contentJson = jsonEncode(content.toJson());
final update = MessagesCompanion( final update = MessagesCompanion(
contactId: Value(fromUserId), contactId: Value(fromUserId),
kind: Value(message.kind), kind: Value(message.kind),
messageOtherId: Value(message.messageSenderId), messageOtherId: Value(message.messageSenderId),
contentJson: Value(contentJson), contentJson: Value(contentJson),
acknowledgeByServer: Value(true), acknowledgeByServer: const Value(true),
acknowledgeByUser: Value(acknowledgeByUser), acknowledgeByUser: Value(acknowledgeByUser),
responseToMessageId: Value(responseToMessageId), responseToMessageId: Value(responseToMessageId),
responseToOtherMessageId: Value(responseToOtherMessageId), responseToOtherMessageId: Value(responseToOtherMessageId),
@ -403,7 +400,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
} }
if (message.kind == MessageKind.media) { if (message.kind == MessageKind.media) {
twonlyDB.contactsDao.incFlameCounter( await twonlyDB.contactsDao.incFlameCounter(
fromUserId, fromUserId,
true, true,
message.timestamp, message.timestamp,
@ -413,37 +410,37 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
.getMessageByMessageId(messageId) .getMessageByMessageId(messageId)
.getSingleOrNull(); .getSingleOrNull();
if (msg != null) { if (msg != null) {
startDownloadMedia(msg, false); unawaited(startDownloadMedia(msg, false));
} }
} }
} else { } else {
Log.error("Content is not defined $message"); Log.error('Content is not defined $message');
} }
// unarchive contact when receiving a new message // unarchive contact when receiving a new message
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
fromUserId, fromUserId,
ContactsCompanion( const ContactsCompanion(
archived: Value(false), archived: Value(false),
), ),
); );
} }
} }
var ok = client.Response_Ok()..none = true; final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
Future<client.Response> handleRequestNewPreKey() async { Future<client.Response> handleRequestNewPreKey() async {
List<PreKeyRecord> localPreKeys = await signalGetPreKeys(); final localPreKeys = await signalGetPreKeys();
List<client.Response_PreKey> prekeysList = []; final prekeysList = <client.Response_PreKey>[];
for (int i = 0; i < localPreKeys.length; i++) { for (var i = 0; i < localPreKeys.length; i++) {
prekeysList.add(client.Response_PreKey() prekeysList.add(client.Response_PreKey()
..id = Int64(localPreKeys[i].id) ..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize()); ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
} }
var prekeys = client.Response_Prekeys(prekeys: prekeysList); final prekeys = client.Response_Prekeys(prekeys: prekeysList);
var ok = client.Response_Ok()..prekeys = prekeys; final ok = client.Response_Ok()..prekeys = prekeys;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
@ -451,18 +448,18 @@ Future<client.Response> handleContactRequest(
int fromUserId, MessageJson message) async { int fromUserId, MessageJson message) async {
// request the username by the server so an attacker can not // request the username by the server so an attacker can not
// forge the displayed username in the contact request // forge the displayed username in the contact request
Result username = await apiService.getUsername(fromUserId); final username = await apiService.getUsername(fromUserId);
if (username.isSuccess) { if (username.isSuccess) {
Uint8List name = username.value.userdata.username; final name = username.value.userdata.username as Uint8List;
await twonlyDB.contactsDao.insertContact( await twonlyDB.contactsDao.insertContact(
ContactsCompanion( ContactsCompanion(
username: Value(utf8.decode(name)), username: Value(utf8.decode(name)),
userId: Value(fromUserId), userId: Value(fromUserId),
requested: Value(true), requested: const Value(true),
), ),
); );
} }
await setupNotificationWithUsers(); await setupNotificationWithUsers();
var ok = client.Response_Ok()..none = true; final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }

View file

@ -14,16 +14,17 @@ import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
class Result<T, E> { class Result<T, E> {
Result.error(this.error) : value = null;
Result.success(this.value) : error = null;
final T? value; final T? value;
final E? error; final E? error;
bool get isSuccess => value != null; bool get isSuccess => value != null;
bool get isError => error != null; bool get isError => error != null;
Result.success(this.value) : error = null;
Result.error(this.error) : value = null;
} }
// ignore: strict_raw_type
Result asResult(server.ServerToClient? msg) { Result asResult(server.ServerToClient? msg) {
if (msg == null) { if (msg == null) {
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
@ -44,20 +45,20 @@ ClientToServer createClientToServerFromHandshake(Handshake handshake) {
ClientToServer createClientToServerFromApplicationData( ClientToServer createClientToServerFromApplicationData(
ApplicationData applicationData) { ApplicationData applicationData) {
var v0 = client.V0() final v0 = client.V0()
..seq = Int64(0) ..seq = Int64(0)
..applicationdata = applicationData; ..applicationdata = applicationData;
return ClientToServer()..v0 = v0; return ClientToServer()..v0 = v0;
} }
Future deleteContact(int contactId) async { Future<void> deleteContact(int contactId) async {
await twonlyDB.messagesDao.deleteAllMessagesByContactId(contactId); await twonlyDB.messagesDao.deleteAllMessagesByContactId(contactId);
await twonlyDB.signalDao.deleteAllByContactId(contactId); await twonlyDB.signalDao.deleteAllByContactId(contactId);
await deleteSessionWithTarget(contactId); await deleteSessionWithTarget(contactId);
await twonlyDB.contactsDao.deleteContactByUserId(contactId); await twonlyDB.contactsDao.deleteContactByUserId(contactId);
} }
Future rejectUser(int contactId) async { Future<void> rejectUser(int contactId) async {
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
contactId, contactId,
@ -69,10 +70,10 @@ Future rejectUser(int contactId) async {
); );
} }
Future handleMediaError(Message message) async { Future<void> handleMediaError(Message message) async {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId, message.messageId,
MessagesCompanion( const MessagesCompanion(
errorWhileSending: Value(true), errorWhileSending: Value(true),
mediaRetransmissionState: Value( mediaRetransmissionState: Value(
MediaRetransmitting.requested, MediaRetransmitting.requested,
@ -80,7 +81,7 @@ Future handleMediaError(Message message) async {
), ),
); );
if (message.messageOtherId != null) { if (message.messageOtherId != null) {
encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
message.contactId, message.contactId,
MessageJson( MessageJson(

View file

@ -1,3 +1,5 @@
import 'dart:io' show Platform;
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -5,22 +7,22 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'dart:io' show Platform;
import '../../firebase_options.dart'; import '../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
Future initFCMAfterAuthenticated() async { Future<void> initFCMAfterAuthenticated() async {
if (globalIsAppInBackground) return; if (globalIsAppInBackground) return;
final storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
String? storedToken = await storage.read(key: SecureStorageKeys.googleFcm); final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
try { try {
final fcmToken = await FirebaseMessaging.instance.getToken(); final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) { if (fcmToken == null) {
Log.error("Error getting fcmToken"); Log.error('Error getting fcmToken');
return; return;
} }
@ -33,14 +35,14 @@ Future initFCMAfterAuthenticated() async {
await apiService.updateFCMToken(fcmToken); await apiService.updateFCMToken(fcmToken);
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken); await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
}).onError((err) { }).onError((err) {
Log.error("could not listen on token refresh"); Log.error('could not listen on token refresh');
}); });
} catch (e) { } catch (e) {
Log.error("could not load fcm token: $e"); Log.error('could not load fcm token: $e');
} }
} }
Future initFCMService() async { Future<void> initFCMService() async {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
@ -51,15 +53,7 @@ Future initFCMService() async {
// of notifications they would like to receive once the user receives a notification. // of notifications they would like to receive once the user receives a notification.
// final notificationSettings = // final notificationSettings =
// await FirebaseMessaging.instance.requestPermission(provisional: true); // await FirebaseMessaging.instance.requestPermission(provisional: true);
await FirebaseMessaging.instance.requestPermission( await FirebaseMessaging.instance.requestPermission();
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
// For apple platforms, ensure the APNS token is available before making any FCM plugin API calls // For apple platforms, ensure the APNS token is available before making any FCM plugin API calls
if (Platform.isIOS) { if (Platform.isIOS) {
@ -69,9 +63,7 @@ Future initFCMService() async {
} }
} }
FirebaseMessaging.onMessage.listen((RemoteMessage message) { FirebaseMessaging.onMessage.listen(handleRemoteMessage);
handleRemoteMessage(message);
});
} }
@pragma('vm:entry-point') @pragma('vm:entry-point')
@ -80,19 +72,19 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
Log.info('Handling a background message: ${message.messageId}'); Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message); await handleRemoteMessage(message);
// make sure every thing run... // make sure every thing run...
await Future.delayed(Duration(milliseconds: 2000)); await Future.delayed(const Duration(milliseconds: 2000));
} }
Future handleRemoteMessage(RemoteMessage message) async { Future<void> handleRemoteMessage(RemoteMessage message) async {
if (!Platform.isAndroid) { if (!Platform.isAndroid) {
Log.error("Got message in Dart while on iOS"); Log.error('Got message in Dart while on iOS');
} }
if (message.notification != null) { if (message.notification != null) {
String title = message.notification!.title ?? ""; final title = message.notification!.title ?? '';
String body = message.notification!.body ?? ""; final body = message.notification!.body ?? '';
await customLocalPushNotification(title, body); await customLocalPushNotification(title, body);
} else if (message.data["push_data"] != null) { } else if (message.data['push_data'] != null) {
await handlePushData(message.data["push_data"]); await handlePushData(message.data['push_data'] as String);
} }
} }

View file

@ -9,7 +9,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/model/json/message.dart' as my; import 'package:twonly/src/model/json/message.dart' as my;
Future syncFlameCounters() async { Future<void> syncFlameCounters() async {
var user = await getUser(); var user = await getUser();
if (user == null) return; if (user == null) return;

View file

@ -14,9 +14,8 @@ import 'package:twonly/src/utils/log.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
Future customLocalPushNotification(String title, String msg) async { Future<void> customLocalPushNotification(String title, String msg) async {
const AndroidNotificationDetails androidNotificationDetails = const androidNotificationDetails = AndroidNotificationDetails(
AndroidNotificationDetails(
'1', '1',
'System', 'System',
channelDescription: 'System messages.', channelDescription: 'System messages.',
@ -24,9 +23,8 @@ Future customLocalPushNotification(String title, String msg) async {
priority: Priority.max, priority: Priority.max,
); );
const DarwinNotificationDetails darwinNotificationDetails = const darwinNotificationDetails = DarwinNotificationDetails();
DarwinNotificationDetails(); const notificationDetails = NotificationDetails(
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, android: androidNotificationDetails,
iOS: darwinNotificationDetails, iOS: darwinNotificationDetails,
); );
@ -39,7 +37,7 @@ Future customLocalPushNotification(String title, String msg) async {
); );
} }
Future handlePushData(String pushDataB64) async { Future<void> handlePushData(String pushDataB64) async {
try { try {
final pushData = final pushData =
EncryptedPushNotification.fromBuffer(base64.decode(pushDataB64)); EncryptedPushNotification.fromBuffer(base64.decode(pushDataB64));
@ -48,7 +46,7 @@ Future handlePushData(String pushDataB64) async {
PushUser? foundPushUser; PushUser? foundPushUser;
if (pushData.keyId == 0) { if (pushData.keyId == 0) {
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits; final key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
pushNotification = await tryDecryptMessage(key, pushData); pushNotification = await tryDecryptMessage(key, pushData);
} else { } else {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
@ -70,14 +68,14 @@ Future handlePushData(String pushDataB64) async {
if (pushNotification != null) { if (pushNotification != null) {
if (pushNotification.kind == PushKind.testNotification) { if (pushNotification.kind == PushKind.testNotification) {
await customLocalPushNotification( await customLocalPushNotification(
"Test notification", 'Test notification',
"This is a test notification.", 'This is a test notification.',
); );
} else if (foundPushUser != null) { } else if (foundPushUser != null) {
if (pushNotification.hasMessageId()) { if (pushNotification.hasMessageId()) {
if (pushNotification.messageId <= foundPushUser.lastMessageId) { if (pushNotification.messageId <= foundPushUser.lastMessageId) {
Log.info( Log.info(
"Got a push notification for a message which was already opened.", 'Got a push notification for a message which was already opened.',
); );
return; return;
} }
@ -90,8 +88,8 @@ Future handlePushData(String pushDataB64) async {
} }
} catch (e) { } catch (e) {
await customLocalPushNotification( await customLocalPushNotification(
"Du hast eine neue Nachricht.", 'Du hast eine neue Nachricht.',
"Öffne twonly um mehr zu erfahren.", 'Öffne twonly um mehr zu erfahren.',
); );
Log.error(e); Log.error(e);
} }
@ -101,9 +99,9 @@ Future<PushNotification?> tryDecryptMessage(
List<int> key, EncryptedPushNotification push) async { List<int> key, EncryptedPushNotification push) async {
try { try {
final chacha20 = FlutterChacha20.poly1305Aead(); final chacha20 = FlutterChacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(key); final secretKeyData = SecretKeyData(key);
SecretBox secretBox = SecretBox( final secretBox = SecretBox(
push.ciphertext, push.ciphertext,
nonce: push.nonce, nonce: push.nonce,
mac: Mac(push.mac), mac: Mac(push.mac),
@ -118,7 +116,7 @@ Future<PushNotification?> tryDecryptMessage(
} }
} }
Future showLocalPushNotification( Future<void> showLocalPushNotification(
PushUser pushUser, PushUser pushUser,
PushNotification pushNotification, PushNotification pushNotification,
) async { ) async {
@ -127,24 +125,23 @@ Future showLocalPushNotification(
// do not show notification for blocked users... // do not show notification for blocked users...
if (pushUser.blocked) { if (pushUser.blocked) {
Log.info("Blocked a message from a blocked user!"); Log.info('Blocked a message from a blocked user!');
return; return;
} }
title = pushUser.displayName; title = pushUser.displayName;
body = getPushNotificationText(pushNotification); body = getPushNotificationText(pushNotification);
if (body == "") { if (body == '') {
Log.error("No push notification type defined!"); Log.error('No push notification type defined!');
} }
FilePathAndroidBitmap? styleInformation; FilePathAndroidBitmap? styleInformation;
String? avatarPath = await getAvatarIcon(pushUser.userId.toInt()); final avatarPath = await getAvatarIcon(pushUser.userId.toInt());
if (avatarPath != null) { if (avatarPath != null) {
styleInformation = FilePathAndroidBitmap(avatarPath); styleInformation = FilePathAndroidBitmap(avatarPath);
} }
AndroidNotificationDetails androidNotificationDetails = final androidNotificationDetails = AndroidNotificationDetails(
AndroidNotificationDetails(
'0', '0',
'Messages', 'Messages',
channelDescription: 'Messages from other users.', channelDescription: 'Messages from other users.',
@ -154,9 +151,8 @@ Future showLocalPushNotification(
largeIcon: styleInformation, largeIcon: styleInformation,
); );
const DarwinNotificationDetails darwinNotificationDetails = const darwinNotificationDetails = DarwinNotificationDetails();
DarwinNotificationDetails(); final notificationDetails = NotificationDetails(
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, android: androidNotificationDetails,
iOS: darwinNotificationDetails, iOS: darwinNotificationDetails,
); );
@ -170,19 +166,18 @@ Future showLocalPushNotification(
); );
} }
Future showLocalPushNotificationWithoutUserId( Future<void> showLocalPushNotificationWithoutUserId(
PushNotification pushNotification, PushNotification pushNotification,
) async { ) async {
String? title; String? title;
String? body; String? body;
body = getPushNotificationTextWithoutUserId(pushNotification.kind); body = getPushNotificationTextWithoutUserId(pushNotification.kind);
if (body == "") { if (body == '') {
Log.error("No push notification type defined!"); Log.error('No push notification type defined!');
} }
AndroidNotificationDetails androidNotificationDetails = const androidNotificationDetails = AndroidNotificationDetails(
AndroidNotificationDetails(
'0', '0',
'Messages', 'Messages',
channelDescription: 'Messages from other users.', channelDescription: 'Messages from other users.',
@ -191,9 +186,8 @@ Future showLocalPushNotificationWithoutUserId(
ticker: 'You got a new message.', ticker: 'You got a new message.',
); );
const DarwinNotificationDetails darwinNotificationDetails = const darwinNotificationDetails = DarwinNotificationDetails();
DarwinNotificationDetails(); const notificationDetails = NotificationDetails(
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails); android: androidNotificationDetails, iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
@ -219,99 +213,99 @@ Future<String?> getAvatarIcon(int contactId) async {
String getPushNotificationTextWithoutUserId(PushKind pushKind) { String getPushNotificationTextWithoutUserId(PushKind pushKind) {
Map<String, String> pushNotificationText; Map<String, String> pushNotificationText;
String systemLanguage = Platform.localeName; final systemLanguage = Platform.localeName;
if (systemLanguage.contains("de")) { if (systemLanguage.contains('de')) {
pushNotificationText = { pushNotificationText = {
PushKind.text.name: "Du hast eine neue Nachricht erhalten.", PushKind.text.name: 'Du hast eine neue Nachricht erhalten.',
PushKind.twonly.name: "Du hast ein neues twonly erhalten.", PushKind.twonly.name: 'Du hast ein neues twonly erhalten.',
PushKind.video.name: "Du hast ein neues Video erhalten.", PushKind.video.name: 'Du hast ein neues Video erhalten.',
PushKind.image.name: "Du hast ein neues Bild erhalten.", PushKind.image.name: 'Du hast ein neues Bild erhalten.',
PushKind.contactRequest.name: PushKind.contactRequest.name:
"Du hast eine neue Kontaktanfrage erhalten.", 'Du hast eine neue Kontaktanfrage erhalten.',
PushKind.acceptRequest.name: "Deine Kontaktanfrage wurde angenommen.", PushKind.acceptRequest.name: 'Deine Kontaktanfrage wurde angenommen.',
PushKind.storedMediaFile.name: "Dein Bild wurde gespeichert.", PushKind.storedMediaFile.name: 'Dein Bild wurde gespeichert.',
PushKind.reaction.name: "Du hast eine Reaktion auf dein Bild erhalten.", PushKind.reaction.name: 'Du hast eine Reaktion auf dein Bild erhalten.',
PushKind.reopenedMedia.name: "Dein Bild wurde erneut geöffnet.", PushKind.reopenedMedia.name: 'Dein Bild wurde erneut geöffnet.',
PushKind.reactionToVideo.name: PushKind.reactionToVideo.name:
"Du hast eine Reaktion auf dein Video erhalten.", 'Du hast eine Reaktion auf dein Video erhalten.',
PushKind.reactionToText.name: PushKind.reactionToText.name:
"Du hast eine Reaktion auf deinen Text erhalten.", 'Du hast eine Reaktion auf deinen Text erhalten.',
PushKind.reactionToImage.name: PushKind.reactionToImage.name:
"Du hast eine Reaktion auf dein Bild erhalten.", 'Du hast eine Reaktion auf dein Bild erhalten.',
PushKind.response.name: "Du hast eine Antwort erhalten.", PushKind.response.name: 'Du hast eine Antwort erhalten.',
}; };
} else { } else {
pushNotificationText = { pushNotificationText = {
PushKind.text.name: "You have received a new message.", PushKind.text.name: 'You have received a new message.',
PushKind.twonly.name: "You have received a new twonly.", PushKind.twonly.name: 'You have received a new twonly.',
PushKind.video.name: "You have received a new video.", PushKind.video.name: 'You have received a new video.',
PushKind.image.name: "You have received a new image.", PushKind.image.name: 'You have received a new image.',
PushKind.contactRequest.name: "You have received a new contact request.", PushKind.contactRequest.name: 'You have received a new contact request.',
PushKind.acceptRequest.name: "Your contact request has been accepted.", PushKind.acceptRequest.name: 'Your contact request has been accepted.',
PushKind.storedMediaFile.name: "Your image has been saved.", PushKind.storedMediaFile.name: 'Your image has been saved.',
PushKind.reaction.name: "You have received a reaction to your image.", PushKind.reaction.name: 'You have received a reaction to your image.',
PushKind.reopenedMedia.name: "Your image has been reopened.", PushKind.reopenedMedia.name: 'Your image has been reopened.',
PushKind.reactionToVideo.name: PushKind.reactionToVideo.name:
"You have received a reaction to your video.", 'You have received a reaction to your video.',
PushKind.reactionToText.name: PushKind.reactionToText.name:
"You have received a reaction to your text.", 'You have received a reaction to your text.',
PushKind.reactionToImage.name: PushKind.reactionToImage.name:
"You have received a reaction to your image.", 'You have received a reaction to your image.',
PushKind.response.name: "You have received a response.", PushKind.response.name: 'You have received a response.',
}; };
} }
return pushNotificationText[pushKind.name] ?? ""; return pushNotificationText[pushKind.name] ?? '';
} }
String getPushNotificationText(PushNotification pushNotification) { String getPushNotificationText(PushNotification pushNotification) {
String systemLanguage = Platform.localeName; final systemLanguage = Platform.localeName;
Map<String, String> pushNotificationText; Map<String, String> pushNotificationText;
if (systemLanguage.contains("de")) { if (systemLanguage.contains('de')) {
pushNotificationText = { pushNotificationText = {
PushKind.text.name: "hat dir eine Nachricht gesendet.", PushKind.text.name: 'hat dir eine Nachricht gesendet.',
PushKind.twonly.name: "hat dir ein twonly gesendet.", PushKind.twonly.name: 'hat dir ein twonly gesendet.',
PushKind.video.name: "hat dir ein Video gesendet.", PushKind.video.name: 'hat dir ein Video gesendet.',
PushKind.image.name: "hat dir ein Bild gesendet.", PushKind.image.name: 'hat dir ein Bild gesendet.',
PushKind.contactRequest.name: "möchte sich mit dir vernetzen.", PushKind.contactRequest.name: 'möchte sich mit dir vernetzen.',
PushKind.acceptRequest.name: "ist jetzt mit dir vernetzt.", PushKind.acceptRequest.name: 'ist jetzt mit dir vernetzt.',
PushKind.storedMediaFile.name: "hat dein Bild gespeichert.", PushKind.storedMediaFile.name: 'hat dein Bild gespeichert.',
PushKind.reaction.name: "hat auf dein Bild reagiert.", PushKind.reaction.name: 'hat auf dein Bild reagiert.',
PushKind.reopenedMedia.name: "hat dein Bild erneut geöffnet.", PushKind.reopenedMedia.name: 'hat dein Bild erneut geöffnet.',
PushKind.reactionToVideo.name: PushKind.reactionToVideo.name:
"hat mit {{reaction}} auf dein Video reagiert.", 'hat mit {{reaction}} auf dein Video reagiert.',
PushKind.reactionToText.name: PushKind.reactionToText.name:
"hat mit {{reaction}} auf deine Nachricht reagiert.", 'hat mit {{reaction}} auf deine Nachricht reagiert.',
PushKind.reactionToImage.name: PushKind.reactionToImage.name:
"hat mit {{reaction}} auf dein Bild reagiert.", 'hat mit {{reaction}} auf dein Bild reagiert.',
PushKind.response.name: "hat dir geantwortet.", PushKind.response.name: 'hat dir geantwortet.',
}; };
} else { } else {
pushNotificationText = { pushNotificationText = {
PushKind.text.name: "has sent you a message.", PushKind.text.name: 'has sent you a message.',
PushKind.twonly.name: "has sent you a twonly.", PushKind.twonly.name: 'has sent you a twonly.',
PushKind.video.name: "has sent you a video.", PushKind.video.name: 'has sent you a video.',
PushKind.image.name: "has sent you an image.", PushKind.image.name: 'has sent you an image.',
PushKind.contactRequest.name: "wants to connect with you.", PushKind.contactRequest.name: 'wants to connect with you.',
PushKind.acceptRequest.name: "is now connected with you.", PushKind.acceptRequest.name: 'is now connected with you.',
PushKind.storedMediaFile.name: "has stored your image.", PushKind.storedMediaFile.name: 'has stored your image.',
PushKind.reaction.name: "has reacted to your image.", PushKind.reaction.name: 'has reacted to your image.',
PushKind.reopenedMedia.name: "has reopened your image.", PushKind.reopenedMedia.name: 'has reopened your image.',
PushKind.reactionToVideo.name: PushKind.reactionToVideo.name:
"has reacted with {{reaction}} to your video.", 'has reacted with {{reaction}} to your video.',
PushKind.reactionToText.name: PushKind.reactionToText.name:
"has reacted with {{reaction}} to your message.", 'has reacted with {{reaction}} to your message.',
PushKind.reactionToImage.name: PushKind.reactionToImage.name:
"has reacted with {{reaction}} to your image.", 'has reacted with {{reaction}} to your image.',
PushKind.response.name: "has responded.", PushKind.response.name: 'has responded.',
}; };
} }
var contentText = pushNotificationText[pushNotification.kind.name] ?? ""; var contentText = pushNotificationText[pushNotification.kind.name] ?? '';
if (pushNotification.hasReactionContent()) { if (pushNotification.hasReactionContent()) {
contentText = contentText.replaceAll( contentText = contentText.replaceAll(
"{{reaction}}", pushNotification.reactionContent); '{{reaction}}', pushNotification.reactionContent);
} }
return contentText; return contentText;
} }

View file

@ -18,20 +18,20 @@ import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
/// This function must be called after the database is setup /// This function must be called after the database is setup
Future setupNotificationWithUsers( Future<void> setupNotificationWithUsers(
{bool force = false, int? forceContact}) async { {bool force = false, int? forceContact}) async {
var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
// HotFIX: Search for user with id 0 if not there remove all // HotFIX: Search for user with id 0 if not there remove all
// and create new push keys with all users. // and create new push keys with all users.
PushUser? pushUser = pushUsers.firstWhereOrNull((x) => x.userId == 0); final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == 0);
if (pushUser == null) { if (pushUser == null) {
Log.info("Clearing push keys"); Log.info('Clearing push keys');
await setPushKeys(SecureStorageKeys.receivingPushKeys, []); await setPushKeys(SecureStorageKeys.receivingPushKeys, []);
pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys)
pushUsers.add(PushUser( ..add(PushUser(
userId: Int64(0), userId: Int64(),
displayName: "NoUser", displayName: 'NoUser',
pushKeys: [], pushKeys: [],
)); ));
} }
@ -42,7 +42,7 @@ Future setupNotificationWithUsers(
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
for (final contact in contacts) { for (final contact in contacts) {
PushUser? pushUser = final pushUser =
pushUsers.firstWhereOrNull((x) => x.userId == contact.userId); pushUsers.firstWhereOrNull((x) => x.userId == contact.userId);
if (pushUser != null) { if (pushUser != null) {
@ -67,11 +67,11 @@ Future setupNotificationWithUsers(
pushUser.pushKeys.add(lastKey); pushUser.pushKeys.add(lastKey);
pushUser.pushKeys.add(pushKey); pushUser.pushKeys.add(pushKey);
wasChanged = true; wasChanged = true;
Log.info("Creating new pushkey for ${contact.userId}"); Log.info('Creating new pushkey for ${contact.userId}');
} }
} else { } else {
Log.info( Log.info(
"User ${contact.userId} not yet in pushkeys. Creating a new user.", 'User ${contact.userId} not yet in pushkeys. Creating a new user.',
); );
wasChanged = true; wasChanged = true;
@ -87,7 +87,6 @@ Future setupNotificationWithUsers(
displayName: getContactDisplayName(contact), displayName: getContactDisplayName(contact),
blocked: contact.blocked, blocked: contact.blocked,
pushKeys: [pushKey], pushKeys: [pushKey],
lastMessageId: null,
)); ));
} }
} }
@ -97,7 +96,7 @@ Future setupNotificationWithUsers(
} }
} }
Future sendNewPushKey(int userId, PushKey pushKey) async { Future<void> sendNewPushKey(int userId, PushKey pushKey) async {
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
userId, userId,
@ -114,11 +113,10 @@ Future sendNewPushKey(int userId, PushKey pushKey) async {
); );
} }
Future updatePushUser(Contact contact) async { Future<void> updatePushUser(Contact contact) async {
var pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys); final pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys);
PushUser? pushUser = final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
if (pushUser == null) { if (pushUser == null) {
pushKeys.add(PushUser( pushKeys.add(PushUser(
@ -126,20 +124,21 @@ Future updatePushUser(Contact contact) async {
displayName: getContactDisplayName(contact), displayName: getContactDisplayName(contact),
pushKeys: [], pushKeys: [],
blocked: contact.blocked, blocked: contact.blocked,
lastMessageId: Int64(0), lastMessageId: Int64(),
)); ));
} else { } else {
pushUser.displayName = getContactDisplayName(contact); pushUser
pushUser.blocked = contact.blocked; ..displayName = getContactDisplayName(contact)
..blocked = contact.blocked;
} }
await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys); await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys);
} }
Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async { Future<void> handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
var pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
PushUser? pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); var pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
if (pushUser == null) { if (pushUser == null) {
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
@ -151,13 +150,13 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
displayName: getContactDisplayName(contact), displayName: getContactDisplayName(contact),
pushKeys: [], pushKeys: [],
blocked: contact.blocked, blocked: contact.blocked,
lastMessageId: Int64(0), lastMessageId: Int64(),
)); ));
pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
} }
if (pushUser == null) { if (pushUser == null) {
Log.error("could not store new push key as no user was found"); Log.error('could not store new push key as no user was found');
} }
// only store the newest key... // only store the newest key...
@ -173,14 +172,12 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys); await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys);
} }
Future updateLastMessageId(int fromUserId, int messageId) async { Future<void> updateLastMessageId(int fromUserId, int messageId) async {
List<PushUser> pushUsers = final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
await getPushKeys(SecureStorageKeys.receivingPushKeys);
PushUser? pushUser = final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == fromUserId);
pushUsers.firstWhereOrNull((x) => x.userId == fromUserId);
if (pushUser == null) { if (pushUser == null) {
setupNotificationWithUsers(); unawaited(setupNotificationWithUsers());
return; return;
} }
@ -193,13 +190,12 @@ Future updateLastMessageId(int fromUserId, int messageId) async {
/// this will trigger a push notification /// this will trigger a push notification
/// push notification only containing the message kind and username /// push notification only containing the message kind and username
Future<Uint8List?> getPushData(int toUserId, PushNotification content) async { Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
final List<PushUser> pushKeys = final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
await getPushKeys(SecureStorageKeys.sendingPushKeys);
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits; var key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
int keyId = 0; var keyId = 0;
PushUser? pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId); final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId);
if (pushUser == null) { if (pushUser == null) {
// user does not have send any push keys // user does not have send any push keys
@ -210,7 +206,7 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
content.kind != PushKind.testNotification) { content.kind != PushKind.testNotification) {
// this will be enforced after every app uses this system... :/ // this will be enforced after every app uses this system... :/
// return null; // return null;
Log.error("Using insecure key as the receiver does not send a push key!"); Log.error('Using insecure key as the receiver does not send a push key!');
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
toUserId, toUserId,
@ -226,7 +222,7 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
key = pushUser.pushKeys.last.key; key = pushUser.pushKeys.last.key;
keyId = pushUser.pushKeys.last.id.toInt(); keyId = pushUser.pushKeys.last.id.toInt();
} catch (e) { } catch (e) {
Log.error("No push notification key found for user $toUserId"); Log.error('No push notification key found for user $toUserId');
return null; return null;
} }
} }
@ -248,39 +244,36 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
} }
Future<List<PushUser>> getPushKeys(String storageKey) async { Future<List<PushUser>> getPushKeys(String storageKey) async {
var storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
String? pushKeysProto = await storage.read( final pushKeysProto = await storage.read(
key: storageKey, key: storageKey,
iOptions: IOSOptions( iOptions: const IOSOptions(
groupId: "CN332ZUGRP.eu.twonly.shared", groupId: 'CN332ZUGRP.eu.twonly.shared',
synchronizable: false,
accessibility: KeychainAccessibility.first_unlock, accessibility: KeychainAccessibility.first_unlock,
), ),
); );
if (pushKeysProto == null) return []; if (pushKeysProto == null) return [];
Uint8List pushKeysRaw = base64Decode(pushKeysProto); final pushKeysRaw = base64Decode(pushKeysProto);
return PushUsers.fromBuffer(pushKeysRaw).users; return PushUsers.fromBuffer(pushKeysRaw).users;
} }
Future setPushKeys(String storageKey, List<PushUser> pushKeys) async { Future<void> setPushKeys(String storageKey, List<PushUser> pushKeys) async {
var storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
await storage.delete( await storage.delete(
key: storageKey, key: storageKey,
iOptions: IOSOptions( iOptions: const IOSOptions(
groupId: "CN332ZUGRP.eu.twonly.shared", groupId: 'CN332ZUGRP.eu.twonly.shared',
synchronizable: false,
accessibility: KeychainAccessibility.first_unlock, accessibility: KeychainAccessibility.first_unlock,
), ),
); );
String jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer()); final jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer());
await storage.write( await storage.write(
key: storageKey, key: storageKey,
value: jsonString, value: jsonString,
iOptions: IOSOptions( iOptions: const IOSOptions(
groupId: "CN332ZUGRP.eu.twonly.shared", groupId: 'CN332ZUGRP.eu.twonly.shared',
synchronizable: false,
accessibility: KeychainAccessibility.first_unlock, accessibility: KeychainAccessibility.first_unlock,
), ),
); );

View file

@ -58,7 +58,7 @@ Future<void> setupPushNotification() async {
); );
} }
Future createPushAvatars() async { Future<void> createPushAvatars() async {
if (!Platform.isAndroid) { if (!Platform.isAndroid) {
return; // avatars currently only shown in Android... return; // avatars currently only shown in Android...
} }

View file

@ -1,11 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
@ -17,19 +16,18 @@ final lockingSignalEncryption = Mutex();
Future<Uint8List?> signalEncryptMessage( Future<Uint8List?> signalEncryptMessage(
int target, Uint8List plaintextContent) async { int target, Uint8List plaintextContent) async {
return await lockingSignalEncryption.protect<Uint8List?>(() async { return lockingSignalEncryption.protect<Uint8List?>(() async {
try { try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!; final signalStore = (await getSignalStore())!;
final address = SignalProtocolAddress(target.toString(), defaultDeviceId); final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
SessionCipher session = SessionCipher.fromStore(signalStore, address); final session = SessionCipher.fromStore(signalStore, address);
SignalContactPreKey? preKey = await getPreKeyByContactId(target); final preKey = await getPreKeyByContactId(target);
SignalContactSignedPreKey? signedPreKey = final signedPreKey = await getSignedPreKeyByContactId(target);
await getSignedPreKeyByContactId(target);
if (signedPreKey != null) { if (signedPreKey != null) {
SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore( final sessionBuilder = SessionBuilder.fromSignalStore(
signalStore, signalStore,
address, address,
); );
@ -45,20 +43,20 @@ Future<Uint8List?> signalEncryptMessage(
); );
} }
ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint( final ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint(
DjbECPublicKey(Uint8List.fromList(signedPreKey.signedPreKey)) DjbECPublicKey(Uint8List.fromList(signedPreKey.signedPreKey))
.serialize(), .serialize(),
1, 1,
); );
Uint8List? tempSignedPreKeySignature = Uint8List.fromList( final Uint8List? tempSignedPreKeySignature = Uint8List.fromList(
signedPreKey.signedPreKeySignature, signedPreKey.signedPreKeySignature,
); );
final IdentityKey? tempIdentityKey = final IdentityKey? tempIdentityKey =
await signalStore.getIdentity(address); await signalStore.getIdentity(address);
if (tempIdentityKey != null) { if (tempIdentityKey != null) {
PreKeyBundle preKeyBundle = PreKeyBundle( final preKeyBundle = PreKeyBundle(
target, target,
defaultDeviceId, defaultDeviceId,
preKey?.preKeyId, preKey?.preKeyId,
@ -72,16 +70,16 @@ Future<Uint8List?> signalEncryptMessage(
try { try {
await sessionBuilder.processPreKeyBundle(preKeyBundle); await sessionBuilder.processPreKeyBundle(preKeyBundle);
} catch (e) { } catch (e) {
Log.error("could not process pre key bundle: $e"); Log.error('could not process pre key bundle: $e');
} }
} else { } else {
Log.error("did not get the identity of the remote address"); Log.error('did not get the identity of the remote address');
} }
} }
final ciphertext = await session.encrypt(plaintextContent); final ciphertext = await session.encrypt(plaintextContent);
var b = BytesBuilder(); final b = BytesBuilder();
b.add(ciphertext.serialize()); b.add(ciphertext.serialize());
b.add(intToBytes(ciphertext.getType())); b.add(intToBytes(ciphertext.getType()));
@ -95,36 +93,34 @@ Future<Uint8List?> signalEncryptMessage(
Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async { Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
try { try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!; final signalStore = (await getSignalStore())!;
SessionCipher session = SessionCipher.fromStore( final session = SessionCipher.fromStore(
signalStore, SignalProtocolAddress(source.toString(), defaultDeviceId)); signalStore, SignalProtocolAddress(source.toString(), defaultDeviceId));
List<Uint8List>? msgs = removeLastXBytes(msg, 4); final msgs = removeLastXBytes(msg, 4);
if (msgs == null) { if (msgs == null) {
Log.error("Message requires at least 4 bytes."); Log.error('Message requires at least 4 bytes.');
return null; return null;
} }
Uint8List body = msgs[0]; final body = msgs[0];
int type = bytesToInt(msgs[1]); final type = bytesToInt(msgs[1]);
Uint8List plaintext; Uint8List plaintext;
if (type == CiphertextMessage.prekeyType) { if (type == CiphertextMessage.prekeyType) {
PreKeySignalMessage pre = PreKeySignalMessage(body); final pre = PreKeySignalMessage(body);
plaintext = await session.decrypt(pre); plaintext = await session.decrypt(pre);
} else if (type == CiphertextMessage.whisperType) { } else if (type == CiphertextMessage.whisperType) {
SignalMessage signalMsg = SignalMessage.fromSerialized(body); final signalMsg = SignalMessage.fromSerialized(body);
plaintext = await session.decryptFromSignal(signalMsg); plaintext = await session.decryptFromSignal(signalMsg);
} else { } else {
Log.error("Type not known: $type"); Log.error('Type not known: $type');
return null; return null;
} }
return MessageJson.fromJson( return MessageJson.fromJson(jsonDecode(
jsonDecode(
utf8.decode( utf8.decode(
gzip.decode(plaintext), gzip.decode(plaintext),
), ),
), ) as Map<String, dynamic>);
);
} catch (e) { } catch (e) {
Log.error(e.toString()); Log.error(e.toString());
return null; return null;

View file

@ -1,12 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/json/signal_identity.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart'; import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/signal_identity.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -20,40 +19,41 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
// This function runs after the clients authenticated with the server. // This function runs after the clients authenticated with the server.
// It then checks if it should update a new session key // It then checks if it should update a new session key
Future signalHandleNewServerConnection() async { Future<void> signalHandleNewServerConnection() async {
final UserData? user = await getUser(); final user = await getUser();
if (user == null) return; if (user == null) return;
if (user.signalLastSignedPreKeyUpdated != null) { if (user.signalLastSignedPreKeyUpdated != null) {
DateTime fortyEightHoursAgo = DateTime.now().subtract(Duration(hours: 48)); final fortyEightHoursAgo =
bool isYoungerThan48Hours = DateTime.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours =
(user.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); (user.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo);
if (isYoungerThan48Hours) { if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated. // The key does live for 48 hours then it expires and a new key is generated.
return; return;
} }
} }
SignedPreKeyRecord? signedPreKey = await _getNewSignalSignedPreKey(); final signedPreKey = await _getNewSignalSignedPreKey();
if (signedPreKey == null) { if (signedPreKey == null) {
Log.error("could not generate a new signed pre key!"); Log.error('could not generate a new signed pre key!');
return; return;
} }
await updateUserdata((user) { await updateUserdata((user) {
user.signalLastSignedPreKeyUpdated = DateTime.now(); user.signalLastSignedPreKeyUpdated = DateTime.now();
return user; return user;
}); });
Result res = await apiService.updateSignedPreKey( final res = await apiService.updateSignedPreKey(
signedPreKey.id, signedPreKey.id,
signedPreKey.getKeyPair().publicKey.serialize(), signedPreKey.getKeyPair().publicKey.serialize(),
signedPreKey.signature, signedPreKey.signature,
); );
if (res.isError) { if (res.isError) {
Log.error("could not update the signed pre key: ${res.error}"); Log.error('could not update the signed pre key: ${res.error}');
await updateUserdata((user) { await updateUserdata((user) {
user.signalLastSignedPreKeyUpdated = null; user.signalLastSignedPreKeyUpdated = null;
return user; return user;
}); });
} else { } else {
Log.info("updated signed pre key"); Log.info('updated signed pre key');
} }
} }
@ -61,7 +61,7 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
final user = await getUser(); final user = await getUser();
if (user == null) return []; if (user == null) return [];
int start = user.currentPreKeyIndexStart; final start = user.currentPreKeyIndexStart;
await updateUserdata((user) { await updateUserdata((user) {
user.currentPreKeyIndexStart += 200; user.currentPreKeyIndexStart += 200;
return user; return user;
@ -77,7 +77,7 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
Future<SignalIdentity?> getSignalIdentity() async { Future<SignalIdentity?> getSignalIdentity() async {
try { try {
final storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
var signalIdentityJson = var signalIdentityJson =
await storage.read(key: SecureStorageKeys.signalIdentity); await storage.read(key: SecureStorageKeys.signalIdentity);
if (signalIdentityJson == null) { if (signalIdentityJson == null) {
@ -85,15 +85,15 @@ Future<SignalIdentity?> getSignalIdentity() async {
} }
final decoded = jsonDecode(signalIdentityJson); final decoded = jsonDecode(signalIdentityJson);
signalIdentityJson = null; signalIdentityJson = null;
return SignalIdentity.fromJson(decoded); return SignalIdentity.fromJson(decoded as Map<String, dynamic>);
} catch (e) { } catch (e) {
Log.error("could not load signal identity: $e"); Log.error('could not load signal identity: $e');
return null; return null;
} }
} }
Future createIfNotExistsSignalIdentity() async { Future<void> createIfNotExistsSignalIdentity() async {
final storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final signalIdentity = await storage.read( final signalIdentity = await storage.read(
key: SecureStorageKeys.signalIdentity, key: SecureStorageKeys.signalIdentity,
@ -106,7 +106,7 @@ Future createIfNotExistsSignalIdentity() async {
final identityKeyPair = generateIdentityKeyPair(); final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true); final registrationId = generateRegistrationId(true);
ConnectSignalProtocolStore signalStore = final signalStore =
ConnectSignalProtocolStore(identityKeyPair, registrationId); ConnectSignalProtocolStore(identityKeyPair, registrationId);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId); final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
@ -133,13 +133,13 @@ Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
return null; return null;
} }
int signedPreKeyId = user.currentSignedPreKeyIndexStart; final signedPreKeyId = user.currentSignedPreKeyIndexStart;
await updateUserdata((user) { await updateUserdata((user) {
user.currentSignedPreKeyIndexStart += 1; user.currentSignedPreKeyIndexStart += 1;
return user; return user;
}); });
final SignedPreKeyRecord signedPreKey = generateSignedPreKey( final signedPreKey = generateSignedPreKey(
identityKeyPair, identityKeyPair,
signedPreKeyId, signedPreKeyId,
); );

View file

@ -23,7 +23,7 @@ Mutex requestNewKeys = Mutex();
DateTime lastPreKeyRequest = DateTime.now().subtract(Duration(hours: 1)); DateTime lastPreKeyRequest = DateTime.now().subtract(Duration(hours: 1));
DateTime lastSignedPreKeyRequest = DateTime.now().subtract(Duration(hours: 1)); DateTime lastSignedPreKeyRequest = DateTime.now().subtract(Duration(hours: 1));
Future requestNewPrekeysForContact(int contactId) async { Future<void> requestNewPrekeysForContact(int contactId) async {
if (lastPreKeyRequest if (lastPreKeyRequest
.isAfter(DateTime.now().subtract(Duration(seconds: 60)))) { .isAfter(DateTime.now().subtract(Duration(seconds: 60)))) {
return; return;
@ -59,7 +59,7 @@ Future<SignalContactPreKey?> getPreKeyByContactId(int contactId) async {
return twonlyDB.signalDao.popPreKeyByContactId(contactId); return twonlyDB.signalDao.popPreKeyByContactId(contactId);
} }
Future requestNewSignedPreKeyForContact(int contactId) async { Future<void> requestNewSignedPreKeyForContact(int contactId) async {
if (lastSignedPreKeyRequest if (lastSignedPreKeyRequest
.isAfter(DateTime.now().subtract(Duration(seconds: 60)))) { .isAfter(DateTime.now().subtract(Duration(seconds: 60)))) {
Log.info("last signed pre request was 60s before"); Log.info("last signed pre request was 60s before");

View file

@ -77,7 +77,7 @@ Future<bool> createNewSignalSession(Response_UserData userData) async {
} }
} }
Future deleteSessionWithTarget(int target) async { Future<void> deleteSessionWithTarget(int target) async {
ConnectSignalProtocolStore? signalStore = await getSignalStore(); ConnectSignalProtocolStore? signalStore = await getSignalStore();
if (signalStore == null) return; if (signalStore == null) return;
final address = SignalProtocolAddress(target.toString(), defaultDeviceId); final address = SignalProtocolAddress(target.toString(), defaultDeviceId);

View file

@ -1,23 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:video_thumbnail/video_thumbnail.dart'; import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:path_provider/path_provider.dart';
Future<void> createThumbnails(String directoryPath) async { Future<void> createThumbnails(String directoryPath) async {
final directory = Directory(directoryPath); final directory = Directory(directoryPath);
final outputDirectory = await getTemporaryDirectory(); final outputDirectory = await getTemporaryDirectory();
if (await directory.exists()) { if (directory.existsSync()) {
final List<FileSystemEntity> files = directory.listSync(); final files = directory.listSync();
for (var file in files) { for (final file in files) {
if (file is File) { if (file is File) {
final String filePath = file.path; final filePath = file.path;
final String fileExtension = filePath.split('.').last.toLowerCase(); final fileExtension = filePath.split('.').last.toLowerCase();
if (['jpg', 'jpeg', 'png'].contains(fileExtension)) { if (['jpg', 'jpeg', 'png'].contains(fileExtension)) {
// Create thumbnail for images // Create thumbnail for images
@ -26,23 +27,18 @@ Future<void> createThumbnails(String directoryPath) async {
final thumbnailFile = final thumbnailFile =
File('${outputDirectory.path}/${file.uri.pathSegments.last}'); File('${outputDirectory.path}/${file.uri.pathSegments.last}');
await thumbnailFile.writeAsBytes(thumbnail!.buffer.asUint8List()); await thumbnailFile.writeAsBytes(thumbnail!.buffer.asUint8List());
print('Thumbnail created for image: ${file.uri.pathSegments.last}');
} else if (['mp4', 'mov', 'avi'].contains(fileExtension)) { } else if (['mp4', 'mov', 'avi'].contains(fileExtension)) {
// Create thumbnail for videos // Create thumbnail for videos
print('Thumbnail created for video: ${file.uri.pathSegments.last}');
} }
} }
} }
} else {
print('Directory does not exist: $directoryPath');
} }
} }
Future createThumbnailsForImage(File file) async { Future<void> createThumbnailsForImage(File file) async {
final String fileExtension = file.path.split('.').last.toLowerCase(); final fileExtension = file.path.split('.').last.toLowerCase();
if (fileExtension != "png") { if (fileExtension != 'png') {
Log.error("Could not create thumbnail for image. $fileExtension != .png"); Log.error('Could not create thumbnail for image. $fileExtension != .png');
return; return;
} }
@ -56,45 +52,44 @@ Future createThumbnailsForImage(File file) async {
); );
if (imageBytesCompressed == null) { if (imageBytesCompressed == null) {
Log.error("Could not compress the image"); Log.error('Could not compress the image');
return; return;
} }
File thumbnailFile = getThumbnailPath(file); final thumbnailFile = getThumbnailPath(file);
await thumbnailFile.writeAsBytes(imageBytesCompressed); await thumbnailFile.writeAsBytes(imageBytesCompressed);
} catch (e) { } catch (e) {
Log.error("Could not compress the image got :$e"); Log.error('Could not compress the image got :$e');
} }
} }
Future createThumbnailsForVideo(File file) async { Future<void> createThumbnailsForVideo(File file) async {
final String fileExtension = file.path.split('.').last.toLowerCase(); final fileExtension = file.path.split('.').last.toLowerCase();
if (fileExtension != "mp4") { if (fileExtension != 'mp4') {
Log.error("Could not create thumbnail for video. $fileExtension != .mp4"); Log.error('Could not create thumbnail for video. $fileExtension != .mp4');
return; return;
} }
try { try {
await VideoThumbnail.thumbnailFile( await VideoThumbnail.thumbnailFile(
video: file.path, video: file.path,
imageFormat: ImageFormat.PNG,
thumbnailPath: getThumbnailPath(file).path, thumbnailPath: getThumbnailPath(file).path,
maxWidth: 450, maxWidth: 450,
quality: 75, quality: 75,
); );
} catch (e) { } catch (e) {
Log.error("Could not create the video thumbnail: $e"); Log.error('Could not create the video thumbnail: $e');
} }
} }
File getThumbnailPath(File file) { File getThumbnailPath(File file) {
String originalFileName = file.uri.pathSegments.last; final originalFileName = file.uri.pathSegments.last;
String fileNameWithoutExtension = originalFileName.split('.').first; final fileNameWithoutExtension = originalFileName.split('.').first;
String fileExtension = originalFileName.split('.').last; var fileExtension = originalFileName.split('.').last;
if (fileExtension == "mp4") { if (fileExtension == 'mp4') {
fileExtension = "png"; fileExtension = 'png';
} }
String newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension'; final newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension';
Directory(file.parent.path).createSync(); Directory(file.parent.path).createSync();
return File(join(file.parent.path, newFileName)); return File(join(file.parent.path, newFileName));
} }

View file

@ -8,7 +8,7 @@ import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
Future enableTwonlySafe(String password) async { Future<void> enableTwonlySafe(String password) async {
final user = await getUser(); final user = await getUser();
if (user == null) return; if (user == null) return;
@ -24,7 +24,7 @@ Future enableTwonlySafe(String password) async {
performTwonlySafeBackup(force: true); performTwonlySafeBackup(force: true);
} }
Future disableTwonlySafe() async { Future<void> disableTwonlySafe() async {
final serverUrl = await getTwonlySafeBackupUrl(); final serverUrl = await getTwonlySafeBackupUrl();
if (serverUrl != null) { if (serverUrl != null) {
try { try {

View file

@ -18,7 +18,7 @@ import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart';
Future performTwonlySafeBackup({bool force = false}) async { Future<void> performTwonlySafeBackup({bool force = false}) async {
final user = await getUser(); final user = await getUser();
if (user == null || user.twonlySafeBackup == null || user.isDemoUser) { if (user == null || user.twonlySafeBackup == null || user.isDemoUser) {
@ -27,38 +27,39 @@ Future performTwonlySafeBackup({bool force = false}) async {
if (user.twonlySafeBackup!.backupUploadState == if (user.twonlySafeBackup!.backupUploadState ==
LastBackupUploadState.pending) { LastBackupUploadState.pending) {
Log.warn("Backup upload is already pending."); Log.warn('Backup upload is already pending.');
return; return;
} }
DateTime? lastUpdateTime = user.twonlySafeBackup!.lastBackupDone; final DateTime? lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) { if (!force && lastUpdateTime != null) {
if (lastUpdateTime.isAfter(DateTime.now().subtract(Duration(days: 1)))) { if (lastUpdateTime
.isAfter(DateTime.now().subtract(const Duration(days: 1)))) {
return; return;
} }
} }
Log.info("Starting new twonly Safe-Backup!"); Log.info('Starting new twonly Safe-Backup!');
final baseDir = (await getApplicationSupportDirectory()).path; final baseDir = (await getApplicationSupportDirectory()).path;
final backupDir = Directory(join(baseDir, "backup_twonly_safe/")); final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
await backupDir.create(recursive: true); await backupDir.create(recursive: true);
final backupDatabaseFile = final backupDatabaseFile =
File(join(backupDir.path, "twonly_database.backup.sqlite")); File(join(backupDir.path, 'twonly_database.backup.sqlite'));
final backupDatabaseFileCleaned = final backupDatabaseFileCleaned =
File(join(backupDir.path, "twonly_database.backup.cleaned.sqlite")); File(join(backupDir.path, 'twonly_database.backup.cleaned.sqlite'));
// copy database // copy database
final originalDatabase = File(join(baseDir, "twonly_database.sqlite")); final originalDatabase = File(join(baseDir, 'twonly_database.sqlite'));
await originalDatabase.copy(backupDatabaseFile.path); await originalDatabase.copy(backupDatabaseFile.path);
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
final backupDB = TwonlyDatabase( final backupDB = TwonlyDatabase(
driftDatabase( driftDatabase(
name: "twonly_database.backup", name: 'twonly_database.backup',
native: DriftNativeOptions( native: DriftNativeOptions(
databaseDirectory: () async { databaseDirectory: () async {
return backupDir; return backupDir;
@ -74,24 +75,26 @@ Future performTwonlySafeBackup({bool force = false}) async {
await backupDB.printTableSizes(); await backupDB.printTableSizes();
backupDB.close(); await backupDB.close();
var secureStorageBackup = {}; // ignore: inference_failure_on_collection_literal
final storage = FlutterSecureStorage(); final secureStorageBackup = {};
const storage = FlutterSecureStorage();
secureStorageBackup[SecureStorageKeys.signalIdentity] = secureStorageBackup[SecureStorageKeys.signalIdentity] =
await storage.read(key: SecureStorageKeys.signalIdentity); await storage.read(key: SecureStorageKeys.signalIdentity);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
await storage.read(key: SecureStorageKeys.signalSignedPreKey); await storage.read(key: SecureStorageKeys.signalSignedPreKey);
var userBackup = await getUser(); final userBackup = await getUser();
if (userBackup == null) return; if (userBackup == null) return;
// FILTER settings which should not be in the backup // FILTER settings which should not be in the backup
userBackup.twonlySafeBackup = null; userBackup
userBackup.lastImageSend = null; ..twonlySafeBackup = null
userBackup.todaysImageCounter = null; ..lastImageSend = null
userBackup.lastPlanBallance = ""; ..todaysImageCounter = null
userBackup.additionalUserInvites = ""; ..lastPlanBallance = ''
userBackup.signalLastSignedPreKeyUpdated = null; ..additionalUserInvites = ''
..signalLastSignedPreKeyUpdated = null;
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup); secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
@ -101,8 +104,8 @@ Future performTwonlySafeBackup({bool force = false}) async {
await backupDatabaseFile.delete(); await backupDatabaseFile.delete();
await backupDatabaseFileCleaned.delete(); await backupDatabaseFileCleaned.delete();
Log.info("twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}"); Log.info('twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}');
Log.info("secureStorageLength = ${jsonEncode(secureStorageBackup).length}"); Log.info('secureStorageLength = ${jsonEncode(secureStorageBackup).length}');
final backupProto = TwonlySafeBackupContent( final backupProto = TwonlySafeBackupContent(
secureStorageJson: jsonEncode(secureStorageBackup), secureStorageJson: jsonEncode(secureStorageBackup),
@ -115,7 +118,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
if (user.twonlySafeBackup!.lastBackupDone == null || if (user.twonlySafeBackup!.lastBackupDone == null ||
user.twonlySafeBackup!.lastBackupDone! user.twonlySafeBackup!.lastBackupDone!
.isAfter(DateTime.now().subtract(Duration(days: 90)))) { .isAfter(DateTime.now().subtract(const Duration(days: 90)))) {
force = true; force = true;
} }
@ -124,7 +127,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
if (lastHash != null && !force) { if (lastHash != null && !force) {
if (backupHash == lastHash) { if (backupHash == lastHash) {
Log.info("Since last backup nothing has changed."); Log.info('Since last backup nothing has changed.');
return; return;
} }
} }
@ -144,25 +147,25 @@ Future performTwonlySafeBackup({bool force = false}) async {
nonce: nonce, nonce: nonce,
); );
final encryptedBackupBytes = (TwonlySafeBackupEncrypted( final encryptedBackupBytes = TwonlySafeBackupEncrypted(
mac: secretBox.mac.bytes, mac: secretBox.mac.bytes,
nonce: nonce, nonce: nonce,
cipherText: secretBox.cipherText, cipherText: secretBox.cipherText,
)).writeToBuffer(); ).writeToBuffer();
Log.info("Backup files created."); Log.info('Backup files created.');
var encryptedBackupBytesFile = final encryptedBackupBytesFile =
File(join(backupDir.path, "twonly_safe.backup")); File(join(backupDir.path, 'twonly_safe.backup'));
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes); await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
Log.info( Log.info(
"Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes."); 'Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.');
if (user.backupServer != null) { if (user.backupServer != null) {
if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) { if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) {
Log.error("Backup is to big for the alternative backup server."); Log.error('Backup is to big for the alternative backup server.');
await updateUserdata((user) { await updateUserdata((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
return user; return user;
@ -172,20 +175,20 @@ Future performTwonlySafeBackup({bool force = false}) async {
} }
final task = UploadTask.fromFile( final task = UploadTask.fromFile(
taskId: "backup", taskId: 'backup',
file: encryptedBackupBytesFile, file: encryptedBackupBytesFile,
httpRequestMethod: "PUT", httpRequestMethod: 'PUT',
url: (await getTwonlySafeBackupUrl())!, url: (await getTwonlySafeBackupUrl())!,
// requiresWiFi: true, // requiresWiFi: true,
priority: 5, priority: 5,
post: 'binary', post: 'binary',
retries: 2, retries: 2,
headers: { headers: {
"Content-Type": "application/octet-stream", 'Content-Type': 'application/octet-stream',
}, },
); );
if (await FileDownloader().enqueue(task)) { if (await FileDownloader().enqueue(task)) {
Log.info("Starting upload from twonly Safe backup."); Log.info('Starting upload from twonly Safe backup.');
await updateUserdata((user) { await updateUserdata((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
user.twonlySafeBackup!.lastBackupDone = DateTime.now(); user.twonlySafeBackup!.lastBackupDone = DateTime.now();
@ -194,15 +197,15 @@ Future performTwonlySafeBackup({bool force = false}) async {
}); });
gUpdateBackupView(); gUpdateBackupView();
} else { } else {
Log.error("Error starting UploadTask for twonly Safe."); Log.error('Error starting UploadTask for twonly Safe.');
} }
} }
Future handleBackupStatusUpdate(TaskStatusUpdate update) async { Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
Log.error( Log.error(
"twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}"); 'twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}');
await updateUserdata((user) { await updateUserdata((user) {
if (user.twonlySafeBackup != null) { if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
@ -211,7 +214,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
}); });
} else if (update.status == TaskStatus.complete) { } else if (update.status == TaskStatus.complete) {
Log.error( Log.error(
"twonly Safe uploaded with status code ${update.responseStatusCode}"); 'twonly Safe uploaded with status code ${update.responseStatusCode}');
await updateUserdata((user) { await updateUserdata((user) {
if (user.twonlySafeBackup != null) { if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = user.twonlySafeBackup!.backupUploadState =
@ -220,7 +223,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
return user; return user;
}); });
} else { } else {
Log.info("Backup is in state: ${update.status}"); Log.info('Backup is in state: ${update.status}');
return; return;
} }
gUpdateBackupView(); gUpdateBackupView();

View file

@ -1,3 +1,5 @@
// ignore_for_file: avoid_dynamic_calls
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
@ -15,19 +17,19 @@ import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future recoverTwonlySafe( Future<void> recoverTwonlySafe(
String username, String username,
String password, String password,
BackupServer? server, BackupServer? server,
) async { ) async {
final (backupId, encryptionKey) = await getMasterKey(password, username); final (backupId, encryptionKey) = await getMasterKey(password, username);
String? backupServerUrl = final backupServerUrl =
await getTwonlySafeBackupUrlFromServer(backupId, server); await getTwonlySafeBackupUrlFromServer(backupId, server);
if (backupServerUrl == null) { if (backupServerUrl == null) {
Log.error("Could not create backup url"); Log.error('Could not create backup url');
throw Exception("Could not create backup server url"); throw Exception('Could not create backup server url');
} }
late Uint8List backupData; late Uint8List backupData;
@ -39,7 +41,7 @@ Future recoverTwonlySafe(
}); });
} catch (e) { } catch (e) {
Log.error('Error fetching backup: $e'); Log.error('Error fetching backup: $e');
throw Exception("Backup server could not be reached. ($e)"); throw Exception('Backup server could not be reached. ($e)');
} }
switch (response.statusCode) { switch (response.statusCode) {
@ -55,19 +57,18 @@ Future recoverTwonlySafe(
throw Exception('Unexpected error: ${response.statusCode}'); throw Exception('Unexpected error: ${response.statusCode}');
} }
return await handleBackupData(encryptionKey, backupData); return handleBackupData(encryptionKey, backupData);
} }
Future handleBackupData( Future<void> handleBackupData(
Uint8List encryptionKey, Uint8List encryptionKey,
Uint8List backupData, Uint8List backupData,
) async { ) async {
TwonlySafeBackupEncrypted encryptedBackup = final encryptedBackup = TwonlySafeBackupEncrypted.fromBuffer(
TwonlySafeBackupEncrypted.fromBuffer(
backupData, backupData,
); );
SecretBox secretBox = SecretBox( final secretBox = SecretBox(
encryptedBackup.cipherText, encryptedBackup.cipherText,
nonce: encryptedBackup.nonce, nonce: encryptedBackup.nonce,
mac: Mac(encryptedBackup.mac), mac: Mac(encryptedBackup.mac),
@ -80,12 +81,12 @@ Future handleBackupData(
final plaintextBytes = gzip.decode(compressedBytes); final plaintextBytes = gzip.decode(compressedBytes);
TwonlySafeBackupContent backupContent = TwonlySafeBackupContent.fromBuffer( final backupContent = TwonlySafeBackupContent.fromBuffer(
plaintextBytes, plaintextBytes,
); );
final baseDir = (await getApplicationSupportDirectory()).path; final baseDir = (await getApplicationSupportDirectory()).path;
final originalDatabase = File(join(baseDir, "twonly_database.sqlite")); final originalDatabase = File(join(baseDir, 'twonly_database.sqlite'));
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase); await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
/// When restoring the last message ID must be increased otherwise /// When restoring the last message ID must be increased otherwise
@ -106,33 +107,33 @@ Future handleBackupData(
if (randomUserId != null) { if (randomUserId != null) {
// for each day add 400 message ids // for each day add 400 message ids
var dummyMessagesCounter = (lastMessageSend + 1) * 400; final dummyMessagesCounter = (lastMessageSend + 1) * 400;
Log.info( Log.info(
"Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago."); 'Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.');
for (var i = 0; i < dummyMessagesCounter; i++) { for (var i = 0; i < dummyMessagesCounter; i++) {
await database.messagesDao.insertMessage( await database.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
contactId: Value(randomUserId), contactId: Value(randomUserId),
kind: Value(MessageKind.ack), kind: const Value(MessageKind.ack),
acknowledgeByServer: Value(true), acknowledgeByServer: const Value(true),
errorWhileSending: Value(true), errorWhileSending: const Value(true),
), ),
); );
} }
await database.messagesDao.deleteAllMessagesByContactId(randomUserId); await database.messagesDao.deleteAllMessagesByContactId(randomUserId);
} }
final storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final secureStorage = jsonDecode(backupContent.secureStorageJson); final secureStorage = jsonDecode(backupContent.secureStorageJson);
await storage.write( await storage.write(
key: SecureStorageKeys.signalIdentity, key: SecureStorageKeys.signalIdentity,
value: secureStorage[SecureStorageKeys.signalIdentity]); value: secureStorage[SecureStorageKeys.signalIdentity] as String);
await storage.write( await storage.write(
key: SecureStorageKeys.signalSignedPreKey, key: SecureStorageKeys.signalSignedPreKey,
value: secureStorage[SecureStorageKeys.signalSignedPreKey]); value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String);
await storage.write( await storage.write(
key: SecureStorageKeys.userData, key: SecureStorageKeys.userData,
value: secureStorage[SecureStorageKeys.userData]); value: secureStorage[SecureStorageKeys.userData] as String);
} }

View file

@ -15,9 +15,9 @@ class KeyValueStore {
final file = File(filePath); final file = File(filePath);
// Check if the file exists // Check if the file exists
if (await file.exists()) { if (file.existsSync()) {
final contents = await file.readAsString(); final contents = await file.readAsString();
return jsonDecode(contents); return jsonDecode(contents) as Map<String, dynamic>;
} else { } else {
return null; // File does not exist return null; // File does not exist
} }

View file

@ -43,13 +43,6 @@ Future<String> loadLogFile() async {
} }
} }
Future cleanLogFile() async {
final str = await loadLogFile();
if (str.contains("secureStorageBytes")) {
deleteLogFile();
}
}
Future<void> _writeLogToFile(LogRecord record) async { Future<void> _writeLogToFile(LogRecord record) async {
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log'); final logFile = File('${directory.path}/app.log');

View file

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -12,9 +13,9 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -51,10 +52,10 @@ Future<String?> saveVideoToGallery(String videoPath) async {
} }
Uint8List getRandomUint8List(int length) { Uint8List getRandomUint8List(int length) {
final Random random = Random.secure(); final random = Random.secure();
final Uint8List randomBytes = Uint8List(length); final randomBytes = Uint8List(length);
for (int i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
randomBytes[i] = random.nextInt(256); // Generate a random byte (0-255) randomBytes[i] = random.nextInt(256); // Generate a random byte (0-255)
} }
@ -62,6 +63,7 @@ Uint8List getRandomUint8List(int length) {
} }
String errorCodeToText(BuildContext context, ErrorCode code) { String errorCodeToText(BuildContext context, ErrorCode code) {
// ignore: exhaustive_cases
switch (code) { switch (code) {
case ErrorCode.InternalError: case ErrorCode.InternalError:
return context.lang.errorInternalError; return context.lang.errorInternalError;
@ -81,22 +83,21 @@ String errorCodeToText(BuildContext context, ErrorCode code) {
return context.lang.errorVoucherInvalid; return context.lang.errorVoucherInvalid;
case ErrorCode.PlanUpgradeNotYearly: case ErrorCode.PlanUpgradeNotYearly:
return context.lang.errorPlanUpgradeNotYearly; return context.lang.errorPlanUpgradeNotYearly;
default:
return code.toString(); // Fallback for unrecognized keys
} }
return code.toString(); // Fallback for unrecognized keys
} }
String formatDuration(int seconds) { String formatDuration(int seconds) {
if (seconds < 60) { if (seconds < 60) {
return '$seconds Sec.'; return '$seconds Sec.';
} else if (seconds < 3600) { } else if (seconds < 3600) {
int minutes = seconds ~/ 60; final minutes = seconds ~/ 60;
return '$minutes Min.'; return '$minutes Min.';
} else if (seconds < 86400) { } else if (seconds < 86400) {
int hours = seconds ~/ 3600; final hours = seconds ~/ 3600;
return '$hours Hrs.'; // Assuming "Stu." is for hours return '$hours Hrs.'; // Assuming "Stu." is for hours
} else { } else {
int days = seconds ~/ 86400; final days = seconds ~/ 86400;
return '$days Days'; return '$days Days';
} }
} }
@ -107,20 +108,19 @@ InputDecoration getInputDecoration(BuildContext context, String hintText) {
return InputDecoration( return InputDecoration(
hintText: hintText, hintText: hintText,
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(9.0), borderRadius: BorderRadius.circular(9),
borderSide: BorderSide(color: primaryColor, width: 1.0), borderSide: BorderSide(color: primaryColor),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8),
borderSide: borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
BorderSide(color: Theme.of(context).colorScheme.outline, width: 1.0),
), ),
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
); );
} }
Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async { Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
var result = await FlutterImageCompress.compressWithList( final result = await FlutterImageCompress.compressWithList(
imageBytes, imageBytes,
quality: 90, quality: 90,
); );
@ -130,8 +130,8 @@ Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
Future<bool> authenticateUser(String localizedReason, Future<bool> authenticateUser(String localizedReason,
{bool force = true}) async { {bool force = true}) async {
try { try {
final LocalAuthentication auth = LocalAuthentication(); final auth = LocalAuthentication();
bool didAuthenticate = await auth.authenticate( final didAuthenticate = await auth.authenticate(
localizedReason: localizedReason, localizedReason: localizedReason,
options: const AuthenticationOptions(useErrorDialogs: false)); options: const AuthenticationOptions(useErrorDialogs: false));
if (didAuthenticate) { if (didAuthenticate) {
@ -147,31 +147,30 @@ Future<bool> authenticateUser(String localizedReason,
} }
Uint8List intToBytes(int value) { Uint8List intToBytes(int value) {
final byteData = ByteData(4); final byteData = ByteData(4)..setInt32(0, value);
byteData.setInt32(0, value, Endian.big);
return byteData.buffer.asUint8List(); return byteData.buffer.asUint8List();
} }
int bytesToInt(Uint8List bytes) { int bytesToInt(Uint8List bytes) {
final byteData = ByteData.sublistView(bytes); final byteData = ByteData.sublistView(bytes);
return byteData.getInt32(0, Endian.big); return byteData.getInt32(0);
} }
List<Uint8List>? removeLastXBytes(Uint8List original, int count) { List<Uint8List>? removeLastXBytes(Uint8List original, int count) {
if (original.length < count) { if (original.length < count) {
return null; return null;
} }
final newList = Uint8List(original.length - count); final newList = Uint8List(original.length - count)
newList.setAll(0, original.sublist(0, original.length - count)); ..setAll(0, original.sublist(0, original.length - count));
final lastXBytes = original.sublist(original.length - count); final lastXBytes = original.sublist(original.length - count);
return [newList, lastXBytes]; return [newList, lastXBytes];
} }
bool isDarkMode(BuildContext context) { bool isDarkMode(BuildContext context) {
ThemeMode? selectedTheme = context.read<SettingsChangeProvider>().themeMode; final selectedTheme = context.read<SettingsChangeProvider>().themeMode;
bool isDarkMode = final isDarkMode =
MediaQuery.of(context).platformBrightness == Brightness.dark; MediaQuery.of(context).platformBrightness == Brightness.dark;
return selectedTheme == ThemeMode.dark || return selectedTheme == ThemeMode.dark ||
@ -188,20 +187,20 @@ bool isToday(DateTime lastImageSend) {
InputDecoration inputTextMessageDeco(BuildContext context) { InputDecoration inputTextMessageDeco(BuildContext context) {
return InputDecoration( return InputDecoration(
hintText: context.lang.chatListDetailInput, hintText: context.lang.chatListDetailInput,
contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
borderSide: borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0), borderRadius: BorderRadius.circular(20),
borderSide: borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0), borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(color: Colors.grey, width: 2.0), borderSide: const BorderSide(color: Colors.grey, width: 2),
), ),
); );
} }
@ -213,8 +212,8 @@ String truncateString(String input, {int maxLength = 20}) {
return input; return input;
} }
Future insertDemoContacts() async { Future<void> insertDemoContacts() async {
List<String> commonUsernames = [ final commonUsernames = <String>[
'James', 'James',
'Mary', 'Mary',
'John', 'John',
@ -236,7 +235,7 @@ Future insertDemoContacts() async {
'Thomas', 'Thomas',
'Karen', 'Karen',
]; ];
final List<Map<String, dynamic>> contactConfigs = [ final contactConfigs = <Map<String, dynamic>>[
{'count': 3, 'requested': true}, {'count': 3, 'requested': true},
{'count': 4, 'requested': false, 'accepted': true}, {'count': 4, 'requested': false, 'accepted': true},
{'count': 1, 'accepted': true, 'blocked': true}, {'count': 1, 'accepted': true, 'blocked': true},
@ -245,43 +244,44 @@ Future insertDemoContacts() async {
{'count': 1, 'requested': false}, {'count': 1, 'requested': false},
]; ];
int counter = 0; var counter = 0;
for (var config in contactConfigs) { for (final config in contactConfigs) {
for (int i = 0; i < config['count']; i++) { for (var i = 0; i < (config['count'] as int); i++) {
if (counter >= commonUsernames.length) { if (counter >= commonUsernames.length) {
break; break;
} }
String username = commonUsernames[counter]; final username = commonUsernames[counter];
int userId = Random().nextInt(1000000); final userId = Random().nextInt(1000000);
await twonlyDB.contactsDao.insertContact( await twonlyDB.contactsDao.insertContact(
ContactsCompanion( ContactsCompanion(
username: Value(username), username: Value(username),
userId: Value(userId), userId: Value(userId),
requested: Value(config['requested'] ?? false), requested: Value(config['requested'] as bool? ?? false),
accepted: Value(config['accepted'] ?? false), accepted: Value(config['accepted'] as bool? ?? false),
blocked: Value(config['blocked'] ?? false), blocked: Value(config['blocked'] as bool? ?? false),
archived: Value(config['archived'] ?? false), archived: Value(config['archived'] as bool? ?? false),
pinned: Value(config['pinned'] ?? false), pinned: Value(config['pinned'] as bool? ?? false),
), ),
); );
if (config['accepted'] ?? false) { if (config['accepted'] as bool? ?? false) {
for (var i = 0; i < 20; i++) { for (var i = 0; i < 20; i++) {
int chatId = Random().nextInt(chatMessages.length); final chatId = Random().nextInt(chatMessages.length);
await twonlyDB.messagesDao.insertMessage( await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
contactId: Value(userId), contactId: Value(userId),
kind: Value(MessageKind.textMessage), kind: const Value(MessageKind.textMessage),
sendAt: Value(chatMessages[chatId][1]), sendAt: Value(chatMessages[chatId][1] as DateTime),
acknowledgeByServer: Value(true), acknowledgeByServer: const Value(true),
acknowledgeByUser: Value(true), acknowledgeByUser: const Value(true),
messageOtherId: messageOtherId:
Value(Random().nextBool() ? Random().nextInt(10000) : null), Value(Random().nextBool() ? Random().nextInt(10000) : null),
// responseToOtherMessageId: Value(content.responseToMessageId), // responseToOtherMessageId: Value(content.responseToMessageId),
// responseToMessageId: Value(content.responseToOtherMessageId), // responseToMessageId: Value(content.responseToOtherMessageId),
downloadState: Value(DownloadState.downloaded), downloadState: const Value(DownloadState.downloaded),
contentJson: Value( contentJson: Value(
jsonEncode(TextMessageContent(text: chatMessages[chatId][0])), jsonEncode(TextMessageContent(
text: chatMessages[chatId][0] as String)),
), ),
), ),
); );
@ -292,91 +292,94 @@ Future insertDemoContacts() async {
} }
} }
Future createFakeDemoData() async { Future<void> createFakeDemoData() async {
await insertDemoContacts(); await insertDemoContacts();
} }
List<List<dynamic>> chatMessages = [ List<List<dynamic>> chatMessages = [
[ [
"Lorem ipsum dolor sit amet.", 'Lorem ipsum dolor sit amet.',
DateTime.now().subtract(Duration(minutes: 20)) DateTime.now().subtract(const Duration(minutes: 20))
], ],
[ [
"Consectetur adipiscing elit.", 'Consectetur adipiscing elit.',
DateTime.now().subtract(Duration(minutes: 19)) DateTime.now().subtract(const Duration(minutes: 19))
], ],
[ [
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
DateTime.now().subtract(Duration(minutes: 18)) DateTime.now().subtract(const Duration(minutes: 18))
],
["Ut enim ad minim veniam.", DateTime.now().subtract(Duration(minutes: 17))],
[
"Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
DateTime.now().subtract(Duration(minutes: 16))
], ],
[ [
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 'Ut enim ad minim veniam.',
DateTime.now().subtract(Duration(minutes: 15)) DateTime.now().subtract(const Duration(minutes: 17))
], ],
[ [
"Excepteur sint occaecat cupidatat non proident.", 'Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
DateTime.now().subtract(Duration(minutes: 14)) DateTime.now().subtract(const Duration(minutes: 16))
], ],
[ [
"Sunt in culpa qui officia deserunt mollit anim id est laborum.", 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
DateTime.now().subtract(Duration(minutes: 13)) DateTime.now().subtract(const Duration(minutes: 15))
], ],
[ [
"Curabitur pretium tincidunt lacus.", 'Excepteur sint occaecat cupidatat non proident.',
DateTime.now().subtract(Duration(minutes: 12)) DateTime.now().subtract(const Duration(minutes: 14))
],
["Nulla facilisi.", DateTime.now().subtract(Duration(minutes: 11))],
[
"Aenean lacinia bibendum nulla sed consectetur.",
DateTime.now().subtract(Duration(minutes: 10))
], ],
[ [
"Sed posuere consectetur est at lobortis.", 'Sunt in culpa qui officia deserunt mollit anim id est laborum.',
DateTime.now().subtract(Duration(minutes: 9)) DateTime.now().subtract(const Duration(minutes: 13))
], ],
[ [
"Vestibulum id ligula porta felis euismod semper.", 'Curabitur pretium tincidunt lacus.',
DateTime.now().subtract(Duration(minutes: 8)) DateTime.now().subtract(const Duration(minutes: 12))
],
['Nulla facilisi.', DateTime.now().subtract(const Duration(minutes: 11))],
[
'Aenean lacinia bibendum nulla sed consectetur.',
DateTime.now().subtract(const Duration(minutes: 10))
], ],
[ [
"Cras justo odio, dapibus ac facilisis in, egestas eget quam.", 'Sed posuere consectetur est at lobortis.',
DateTime.now().subtract(Duration(minutes: 7)) DateTime.now().subtract(const Duration(minutes: 9))
], ],
[ [
"Morbi leo risus, porta ac consectetur ac, vestibulum at eros.", 'Vestibulum id ligula porta felis euismod semper.',
DateTime.now().subtract(Duration(minutes: 6)) DateTime.now().subtract(const Duration(minutes: 8))
], ],
[ [
"Praesent commodo cursus magna, vel scelerisque nisl consectetur et.", 'Cras justo odio, dapibus ac facilisis in, egestas eget quam.',
DateTime.now().subtract(Duration(minutes: 5)) DateTime.now().subtract(const Duration(minutes: 7))
], ],
[ [
"Donec ullamcorper nulla non metus auctor fringilla.", 'Morbi leo risus, porta ac consectetur ac, vestibulum at eros.',
DateTime.now().subtract(Duration(minutes: 4)) DateTime.now().subtract(const Duration(minutes: 6))
], ],
[ [
"Etiam porta sem malesuada magna mollis euismod.", 'Praesent commodo cursus magna, vel scelerisque nisl consectetur et.',
DateTime.now().subtract(Duration(minutes: 3)) DateTime.now().subtract(const Duration(minutes: 5))
], ],
[ [
"Aenean lacinia bibendum nulla sed consectetur.", 'Donec ullamcorper nulla non metus auctor fringilla.',
DateTime.now().subtract(Duration(minutes: 2)) DateTime.now().subtract(const Duration(minutes: 4))
], ],
[ [
"Nullam quis risus eget urna mollis ornare vel eu leo.", 'Etiam porta sem malesuada magna mollis euismod.',
DateTime.now().subtract(Duration(minutes: 1)) DateTime.now().subtract(const Duration(minutes: 3))
], ],
["Curabitur blandit tempus porttitor.", DateTime.now()], [
'Aenean lacinia bibendum nulla sed consectetur.',
DateTime.now().subtract(const Duration(minutes: 2))
],
[
'Nullam quis risus eget urna mollis ornare vel eu leo.',
DateTime.now().subtract(const Duration(minutes: 1))
],
['Curabitur blandit tempus porttitor.', DateTime.now()],
]; ];
String formatDateTime(BuildContext context, DateTime? dateTime) { String formatDateTime(BuildContext context, DateTime? dateTime) {
if (dateTime == null) { if (dateTime == null) {
return "Never"; return 'Never';
} }
final now = DateTime.now(); final now = DateTime.now();
final difference = now.difference(dateTime); final difference = now.difference(dateTime);
@ -390,32 +393,34 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
if (difference.inDays == 0) { if (difference.inDays == 0) {
return time; return time;
} else { } else {
return "$time $date"; return '$time $date';
} }
} }
String formatBytes(int bytes, {int decimalPlaces = 2}) { String formatBytes(int bytes, {int decimalPlaces = 2}) {
if (bytes <= 0) return "0 Bytes"; if (bytes <= 0) return '0 Bytes';
const List<String> units = ["Bytes", "KB", "MB", "GB", "TB"]; const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
final int unitIndex = (log(bytes) / log(1000)).floor(); final unitIndex = (log(bytes) / log(1000)).floor();
final double formattedSize = bytes / pow(1000, unitIndex); final formattedSize = bytes / pow(1000, unitIndex);
return "${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}"; return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}';
} }
String getMessageText(Message message) { String getMessageText(Message message) {
try { try {
if (message.contentJson == null) return ""; if (message.contentJson == null) return '';
return TextMessageContent.fromJson(jsonDecode(message.contentJson!)).text; return TextMessageContent.fromJson(jsonDecode(message.contentJson!) as Map)
.text;
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
return ""; return '';
} }
} }
MediaMessageContent? getMediaContent(Message message) { MediaMessageContent? getMediaContent(Message message) {
try { try {
if (message.contentJson == null) return null; if (message.contentJson == null) return null;
return MediaMessageContent.fromJson(jsonDecode(message.contentJson!)); return MediaMessageContent.fromJson(
jsonDecode(message.contentJson!) as Map);
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
return null; return null;

View file

@ -10,7 +10,7 @@ import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<bool> isUserCreated() async { Future<bool> isUserCreated() async {
UserData? user = await getUser(); final user = await getUser();
if (user == null) { if (user == null) {
return false; return false;
} }
@ -18,8 +18,8 @@ Future<bool> isUserCreated() async {
} }
Future<UserData?> getUser() async { Future<UserData?> getUser() async {
String? userJson = final userJson =
await FlutterSecureStorage().read(key: SecureStorageKeys.userData); await const FlutterSecureStorage().read(key: SecureStorageKeys.userData);
if (userJson == null) { if (userJson == null) {
return null; return null;
} }
@ -28,12 +28,12 @@ Future<UserData?> getUser() async {
final user = UserData.fromJson(userMap); final user = UserData.fromJson(userMap);
return user; return user;
} catch (e) { } catch (e) {
Log.error("Error getting user: $e"); Log.error('Error getting user: $e');
return null; return null;
} }
} }
Future updateUsersPlan(BuildContext context, String planId) async { Future<void> updateUsersPlan(BuildContext context, String planId) async {
context.read<CustomChangeProvider>().plan = planId; context.read<CustomChangeProvider>().plan = planId;
await updateUserdata((user) { await updateUserdata((user) {
@ -42,17 +42,18 @@ Future updateUsersPlan(BuildContext context, String planId) async {
}); });
if (!context.mounted) return; if (!context.mounted) return;
context.read<CustomChangeProvider>().updatePlan(planId); await context.read<CustomChangeProvider>().updatePlan(planId);
} }
Mutex updateProtection = Mutex(); Mutex updateProtection = Mutex();
Future<UserData?> updateUserdata(Function(UserData userData) updateUser) async { Future<UserData?> updateUserdata(
return await updateProtection.protect<UserData?>(() async { UserData Function(UserData userData) updateUser) async {
return updateProtection.protect<UserData?>(() async {
final user = await getUser(); final user = await getUser();
if (user == null) return null; if (user == null) return null;
UserData updated = updateUser(user); final updated = updateUser(user);
FlutterSecureStorage() await const FlutterSecureStorage()
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); .write(key: SecureStorageKeys.userData, value: jsonEncode(updated));
return user; return user;
}); });
@ -63,6 +64,6 @@ Future<bool> deleteLocalUserData() async {
if (appDir.existsSync()) { if (appDir.existsSync()) {
appDir.deleteSync(recursive: true); appDir.deleteSync(recursive: true);
} }
await FlutterSecureStorage().deleteAll(); await const FlutterSecureStorage().deleteAll();
return true; return true;
} }

View file

@ -36,7 +36,7 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
initAsync(); initAsync();
} }
Future initAsync() async { Future<void> initAsync() async {
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1; showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
if (_isDisposed) return; if (_isDisposed) return;
setState(() {}); setState(() {});

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -8,18 +9,18 @@ import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart';
import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart'; import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart';
import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart'; import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart';
import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart'; import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';
int maxVideoRecordingTime = 15; int maxVideoRecordingTime = 15;
@ -43,7 +44,7 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
details.scaleFactor = 1; details.scaleFactor = 1;
} }
CameraController cameraController = CameraController( final cameraController = CameraController(
gCameras[sCameraId], gCameras[sCameraId],
ResolutionPreset.high, ResolutionPreset.high,
enableAudio: enableAudio, enableAudio: enableAudio,
@ -52,7 +53,7 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
await cameraController.initialize().then((_) async { await cameraController.initialize().then((_) async {
await cameraController.setZoomLevel(details.scaleFactor); await cameraController.setZoomLevel(details.scaleFactor);
await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp); await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp);
cameraController await cameraController
.setFlashMode(details.isFlashOn ? FlashMode.always : FlashMode.off); .setFlashMode(details.isFlashOn ? FlashMode.always : FlashMode.off);
await cameraController await cameraController
.getMaxZoomLevel() .getMaxZoomLevel()
@ -60,9 +61,10 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
await cameraController await cameraController
.getMinZoomLevel() .getMinZoomLevel()
.then((double value) => details.minAvailableZoom = value); .then((double value) => details.minAvailableZoom = value);
details.isZoomAble = details.maxAvailableZoom != details.minAvailableZoom; details
details.cameraLoaded = true; ..isZoomAble = details.maxAvailableZoom != details.minAvailableZoom
details.cameraId = sCameraId; ..cameraLoaded = true
..cameraId = sCameraId;
}).catchError((Object e) { }).catchError((Object e) {
Log.error("$e"); Log.error("$e");
}); });
@ -81,13 +83,13 @@ class SelectedCameraDetails {
class CameraPreviewControllerView extends StatefulWidget { class CameraPreviewControllerView extends StatefulWidget {
const CameraPreviewControllerView({ const CameraPreviewControllerView({
super.key,
required this.selectCamera, required this.selectCamera,
required this.isHomeView, required this.isHomeView,
super.key,
this.sendTo, this.sendTo,
}); });
final Contact? sendTo; final Contact? sendTo;
final Function(int sCameraId, bool init, bool enableAudio) selectCamera; final void Function(int sCameraId, bool init, bool enableAudio) selectCamera;
final bool isHomeView; final bool isHomeView;
@override @override
@ -123,13 +125,13 @@ class _CameraPreviewControllerView extends State<CameraPreviewControllerView> {
class CameraPreviewView extends StatefulWidget { class CameraPreviewView extends StatefulWidget {
const CameraPreviewView( const CameraPreviewView(
{super.key, {required this.selectCamera,
this.sendTo, required this.isHomeView,
required this.selectCamera, super.key,
required this.isHomeView}); this.sendTo});
final Contact? sendTo; final Contact? sendTo;
final bool isHomeView; final bool isHomeView;
final Function(int sCameraId, bool init, bool enableAudio) selectCamera; final void Function(int sCameraId, bool init, bool enableAudio) selectCamera;
@override @override
State<CameraPreviewView> createState() => _CameraPreviewViewState(); State<CameraPreviewView> createState() => _CameraPreviewViewState();
@ -171,7 +173,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
? HomeViewState.screenshotController ? HomeViewState.screenshotController
: CameraSendToViewState.screenshotController; : CameraSendToViewState.screenshotController;
void initAsync() async { Future<void> initAsync() async {
hasAudioPermission = await Permission.microphone.isGranted; hasAudioPermission = await Permission.microphone.isGranted;
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
@ -183,12 +185,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
super.dispose(); super.dispose();
} }
Future requestMicrophonePermission() async { Future<void> requestMicrophonePermission() async {
Map<Permission, PermissionStatus> statuses = await [ final statuses = await [
Permission.microphone, Permission.microphone,
].request(); ].request();
if (statuses[Permission.microphone]!.isPermanentlyDenied) { if (statuses[Permission.microphone]!.isPermanentlyDenied) {
openAppSettings(); await openAppSettings();
} else { } else {
hasAudioPermission = await Permission.microphone.isGranted; hasAudioPermission = await Permission.microphone.isGranted;
setState(() {}); setState(() {});
@ -221,7 +223,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Error loading picture: $e'), content: Text('Error loading picture: $e'),
duration: Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
); );
} }
@ -229,7 +231,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
} }
Future takePicture() async { Future<void> takePicture() async {
if (sharePreviewIsShown || isVideoRecording) return; if (sharePreviewIsShown || isVideoRecording) return;
late Future<Uint8List?> imageBytes; late Future<Uint8List?> imageBytes;
@ -242,16 +244,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
showSelfieFlash = true; showSelfieFlash = true;
}); });
} else { } else {
cameraController?.setFlashMode(FlashMode.torch); await cameraController?.setFlashMode(FlashMode.torch);
} }
await Future.delayed(Duration(milliseconds: 1000)); await Future.delayed(const Duration(milliseconds: 1000));
} }
await cameraController?.pausePreview(); await cameraController?.pausePreview();
if (!mounted) return; if (!mounted) return;
cameraController?.setFlashMode( await cameraController?.setFlashMode(
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off); selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off);
if (!mounted) return;
imageBytes = screenshotController.capture( imageBytes = screenshotController.capture(
pixelRatio: MediaQuery.of(context).devicePixelRatio); pixelRatio: MediaQuery.of(context).devicePixelRatio);
@ -263,7 +267,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Future<bool> pushMediaEditor( Future<bool> pushMediaEditor(
Future<Uint8List?>? imageBytes, File? videoFilePath, Future<Uint8List?>? imageBytes, File? videoFilePath,
{bool sharedFromGallery = false}) async { {bool sharedFromGallery = false}) async {
bool? shouldReturn = await Navigator.push( final shouldReturn = await Navigator.push(
context, context,
PageRouteBuilder( PageRouteBuilder(
opaque: false, opaque: false,
@ -281,7 +285,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
transitionDuration: Duration.zero, transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero,
), ),
); ) as bool?;
if (mounted) { if (mounted) {
setState(() { setState(() {
sharePreviewIsShown = false; sharePreviewIsShown = false;
@ -306,15 +310,16 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
bool get isFront => bool get isFront =>
cameraController?.description.lensDirection == CameraLensDirection.front; cameraController?.description.lensDirection == CameraLensDirection.front;
Future onPanUpdate(details) async { Future<void> onPanUpdate(dynamic details) async {
if (isFront) { if (isFront || details == null) {
return; return;
} }
if (cameraController == null) return; if (cameraController == null) return;
if (!cameraController!.value.isInitialized) return; if (!cameraController!.value.isInitialized) return;
selectedCameraDetails.scaleFactor = selectedCameraDetails.scaleFactor =
(baseScaleFactor + (basePanY - details.localPosition.dy) / 30) // ignore: avoid_dynamic_calls
(baseScaleFactor + (basePanY - (details.localPosition.dy as int)) / 30)
.clamp(1, selectedCameraDetails.maxAvailableZoom); .clamp(1, selectedCameraDetails.maxAvailableZoom);
await cameraController!.setZoomLevel(selectedCameraDetails.scaleFactor); await cameraController!.setZoomLevel(selectedCameraDetails.scaleFactor);
@ -323,7 +328,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
} }
Future pickImageFromGallery() async { Future<void> pickImageFromGallery() async {
setState(() { setState(() {
galleryLoadedImageIsShown = true; galleryLoadedImageIsShown = true;
sharePreviewIsShown = true; sharePreviewIsShown = true;
@ -332,7 +337,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
final pickedFile = await picker.pickImage(source: ImageSource.gallery); final pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) { if (pickedFile != null) {
File imageFile = File(pickedFile.path); final imageFile = File(pickedFile.path);
await pushMediaEditor( await pushMediaEditor(
imageFile.readAsBytes(), imageFile.readAsBytes(),
null, null,
@ -345,12 +350,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}); });
} }
Future startVideoRecording() async { Future<void> startVideoRecording() async {
if (cameraController != null && cameraController!.value.isRecordingVideo) { if (cameraController != null && cameraController!.value.isRecordingVideo) {
return; return;
} }
if (hasAudioPermission && videoWithAudio) { if (hasAudioPermission && videoWithAudio) {
await widget.selectCamera( widget.selectCamera(
selectedCameraDetails.cameraId, selectedCameraDetails.cameraId,
false, false,
await Permission.microphone.isGranted && videoWithAudio, await Permission.microphone.isGranted && videoWithAudio,
@ -363,7 +368,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
try { try {
await cameraController?.startVideoRecording(); await cameraController?.startVideoRecording();
videoRecordingTimer = Timer.periodic(Duration(milliseconds: 15), (timer) { videoRecordingTimer =
Timer.periodic(const Duration(milliseconds: 15), (timer) {
setState(() { setState(() {
currentTime = DateTime.now(); currentTime = DateTime.now();
}); });
@ -388,7 +394,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
} }
Future stopVideoRecording() async { Future<void> stopVideoRecording() async {
if (videoRecordingTimer != null) { if (videoRecordingTimer != null) {
videoRecordingTimer?.cancel(); videoRecordingTimer?.cancel();
videoRecordingTimer = null; videoRecordingTimer = null;
@ -404,7 +410,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
sharePreviewIsShown = true; sharePreviewIsShown = true;
}); });
File? videoPathFile; File? videoPathFile;
XFile? videoPath = await cameraController?.stopVideoRecording(); final videoPath = await cameraController?.stopVideoRecording();
if (videoPath != null) { if (videoPath != null) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
// see https://github.com/flutter/flutter/issues/148335 // see https://github.com/flutter/flutter/issues/148335
@ -432,7 +438,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Error: $e'), content: Text('Error: $e'),
duration: Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
); );
} }
@ -464,10 +470,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
baseScaleFactor = selectedCameraDetails.scaleFactor; baseScaleFactor = selectedCameraDetails.scaleFactor;
}); });
// Get the position of the pointer // Get the position of the pointer
RenderBox renderBox = final renderBox =
keyTriggerButton.currentContext?.findRenderObject() as RenderBox; keyTriggerButton.currentContext!.findRenderObject()! as RenderBox;
Offset localPosition = final localPosition = renderBox.globalToLocal(details.globalPosition);
renderBox.globalToLocal(details.globalPosition);
final containerRect = final containerRect =
Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height); Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height);
@ -620,7 +625,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
height: 50, height: 50,
width: 80, width: 80,
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
child: Center( child: const Center(
child: FaIcon( child: FaIcon(
FontAwesomeIcons.photoFilm, FontAwesomeIcons.photoFilm,
color: Colors.white, color: Colors.white,
@ -653,7 +658,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
), ),
), ),
), ),
if (!isVideoRecording) SizedBox(width: 80) if (!isVideoRecording) const SizedBox(width: 80)
], ],
), ),
], ],

View file

@ -31,7 +31,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
super.dispose(); super.dispose();
} }
Future selectCamera(int sCameraId, bool init, bool enableAudio) async { Future<void> selectCamera(int sCameraId, bool init, bool enableAudio) async {
final opts = await initializeCameraController( final opts = await initializeCameraController(
selectedCameraDetails, sCameraId, init, enableAudio); selectedCameraDetails, sCameraId, init, enableAudio);
if (opts != null) { if (opts != null) {
@ -41,7 +41,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
setState(() {}); setState(() {});
} }
Future toggleSelectedCamera() async { Future<void> toggleSelectedCamera() async {
await cameraController?.dispose(); await cameraController?.dispose();
cameraController = null; cameraController = null;
selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false); selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false);

View file

@ -12,7 +12,7 @@ class ImageItem {
if (image != null) load(image); if (image != null) load(image);
} }
Future load(dynamic image) async { Future<void> load(dynamic image) async {
loader = Completer<bool>(); loader = Completer<bool>();
if (image is ImageItem) { if (image is ImageItem) {

View file

@ -6,24 +6,23 @@ import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
/// Emoji layer /// Emoji layer
class EmojiLayer extends StatefulWidget { class EmojiLayer extends StatefulWidget {
const EmojiLayer({
required this.layerData,
super.key,
this.onUpdate,
});
final EmojiLayerData layerData; final EmojiLayerData layerData;
final VoidCallback? onUpdate; final VoidCallback? onUpdate;
const EmojiLayer({
super.key,
required this.layerData,
this.onUpdate,
});
@override @override
createState() => _EmojiLayerState(); State<EmojiLayer> createState() => _EmojiLayerState();
} }
class _EmojiLayerState extends State<EmojiLayer> { class _EmojiLayerState extends State<EmojiLayer> {
double initialRotation = 0; double initialRotation = 0;
Offset initialOffset = Offset.zero; Offset initialOffset = Offset.zero;
Offset initialFocalPoint = Offset.zero; Offset initialFocalPoint = Offset.zero;
double initialScale = 1.0; double initialScale = 1;
bool deleteLayer = false; bool deleteLayer = false;
bool twoPointerWhereDown = false; bool twoPointerWhereDown = false;
final GlobalKey outlineKey = GlobalKey(); final GlobalKey outlineKey = GlobalKey();
@ -94,16 +93,16 @@ class _EmojiLayerState extends State<EmojiLayer> {
if (twoPointerWhereDown == true && details.pointerCount != 2) { if (twoPointerWhereDown == true && details.pointerCount != 2) {
return; return;
} }
final RenderBox outlineBox = final outlineBox =
outlineKey.currentContext!.findRenderObject() as RenderBox; outlineKey.currentContext!.findRenderObject()! as RenderBox;
final RenderBox emojiBox = final emojiBox =
emojiKey.currentContext!.findRenderObject() as RenderBox; emojiKey.currentContext!.findRenderObject()! as RenderBox;
bool isAtTheBottom = final isAtTheBottom =
(widget.layerData.offset.dy + emojiBox.size.height / 2) > (widget.layerData.offset.dy + emojiBox.size.height / 2) >
outlineBox.size.height - 80; outlineBox.size.height - 80;
bool isInTheCenter = MediaQuery.of(context).size.width / 2 - final isInTheCenter = MediaQuery.of(context).size.width / 2 -
30 < 30 <
(widget.layerData.offset.dx + (widget.layerData.offset.dx +
emojiBox.size.width / 2) && emojiBox.size.width / 2) &&
@ -125,9 +124,9 @@ class _EmojiLayerState extends State<EmojiLayer> {
initialRotation + details.rotation; initialRotation + details.rotation;
// Update the position based on the translation // Update the position based on the translation
var dx = (initialOffset.dx) + final dx = (initialOffset.dx) +
(details.focalPoint.dx - initialFocalPoint.dx); (details.focalPoint.dx - initialFocalPoint.dx);
var dy = (initialOffset.dy) + final dy = (initialOffset.dy) +
(details.focalPoint.dy - initialFocalPoint.dy); (details.focalPoint.dy - initialFocalPoint.dy);
widget.layerData.offset = Offset(dx, dy); widget.layerData.offset = Offset(dx, dy);
}); });

View file

@ -7,14 +7,14 @@ import 'package:twonly/src/views/camera/image_editor/layers/filters/location_fil
/// Main layer /// Main layer
class FilterLayer extends StatefulWidget { class FilterLayer extends StatefulWidget {
final FilterLayerData layerData;
// final VoidCallback? onUpdate; // final VoidCallback? onUpdate;
const FilterLayer({ const FilterLayer({
super.key,
required this.layerData, required this.layerData,
super.key,
// this.onUpdate, // this.onUpdate,
}); });
final FilterLayerData layerData;
@override @override
State<FilterLayer> createState() => _FilterLayerState(); State<FilterLayer> createState() => _FilterLayerState();
@ -26,7 +26,7 @@ class FilterSkeleton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return ColoredBox(
color: Colors.transparent, color: Colors.transparent,
child: Stack( child: Stack(
children: [ children: [
@ -39,8 +39,12 @@ class FilterSkeleton extends StatelessWidget {
} }
class FilterText extends StatelessWidget { class FilterText extends StatelessWidget {
const FilterText(this.text, const FilterText(
{super.key, this.fontSize = 24, this.color = Colors.white}); this.text, {
super.key,
this.fontSize = 24,
this.color = Colors.white,
});
final String text; final String text;
final double fontSize; final double fontSize;
final Color color; final Color color;
@ -53,9 +57,9 @@ class FilterText extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: fontSize, fontSize: fontSize,
color: color, color: color,
shadows: [ shadows: const [
Shadow( Shadow(
color: const Color.fromARGB(122, 0, 0, 0), color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5.0, blurRadius: 5.0,
) )
], ],
@ -67,10 +71,10 @@ class FilterText extends StatelessWidget {
class _FilterLayerState extends State<FilterLayer> { class _FilterLayerState extends State<FilterLayer> {
final PageController pageController = PageController(); final PageController pageController = PageController();
List<Widget> pages = [ List<Widget> pages = [
FilterSkeleton(), const FilterSkeleton(),
DateTimeFilter(), const DateTimeFilter(),
LocationFilter(), const LocationFilter(),
FilterSkeleton(), const FilterSkeleton(),
]; ];
@override @override
@ -82,11 +86,11 @@ class _FilterLayerState extends State<FilterLayer> {
initAsync(); initAsync();
} }
Future initAsync() async { Future<void> initAsync() async {
var stickers = (await getStickerIndex()) final stickers = (await getStickerIndex())
.where((x) => x.imageSrc.contains("/imagefilter/")) .where((x) => x.imageSrc.contains('/imagefilter/'))
.toList(); .toList()
stickers.sortBy((x) => x.imageSrc); ..sortBy((x) => x.imageSrc);
for (final sticker in stickers) { for (final sticker in stickers) {
pages.insert(pages.length - 1, ImageFilter(imagePath: sticker.imageSrc)); pages.insert(pages.length - 1, ImageFilter(imagePath: sticker.imageSrc));

View file

@ -1,15 +1,16 @@
import 'dart:convert';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart'; import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart';
import 'package:twonly/src/views/camera/image_editor/layers/filters/datetime_filter.dart'; import 'package:twonly/src/views/camera/image_editor/layers/filters/datetime_filter.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'dart:io';
class LocationFilter extends StatefulWidget { class LocationFilter extends StatefulWidget {
const LocationFilter({super.key}); const LocationFilter({super.key});
@ -28,29 +29,30 @@ class _LocationFilterState extends State<LocationFilter> {
initAsync(); initAsync();
} }
Future initAsync() async { Future<void> initAsync() async {
final res = await apiService.getCurrentLocation(); final res = await apiService.getCurrentLocation();
if (res.isSuccess) { if (res.isSuccess) {
location = res.value.location; // ignore: avoid_dynamic_calls
_searchForImage(); location = res.value.location as Response_Location?;
await _searchForImage();
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
} }
void _searchForImage() async { Future<void> _searchForImage() async {
if (location == null) return; if (location == null) return;
List<Sticker> imageIndex = await getStickerIndex(); final imageIndex = await getStickerIndex();
// Normalize the city and country for search // Normalize the city and country for search
String normalizedCity = location!.city.toLowerCase().replaceAll(' ', '_'); final normalizedCity = location!.city.toLowerCase().replaceAll(' ', '_');
String normalizedCountry = location!.county.toLowerCase(); final normalizedCountry = location!.county.toLowerCase();
// Search for the city first // Search for the city first
for (var item in imageIndex) { for (final item in imageIndex) {
if (item.imageSrc.contains('/cities/$normalizedCountry/')) { if (item.imageSrc.contains('/cities/$normalizedCountry/')) {
// Check if the item matches the normalized city // Check if the item matches the normalized city
if (item.imageSrc.endsWith('$normalizedCity.png')) { if (item.imageSrc.endsWith('$normalizedCity.png')) {
if (item.imageSrc.startsWith("/api/")) { if (item.imageSrc.startsWith('/api/')) {
_imageUrl = "https://twonly.eu/${item.imageSrc}"; _imageUrl = 'https://twonly.eu/${item.imageSrc}';
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
return; return;
@ -60,11 +62,11 @@ class _LocationFilterState extends State<LocationFilter> {
// If city not found, search for the country // If city not found, search for the country
if (_imageUrl == null) { if (_imageUrl == null) {
for (var item in imageIndex) { for (final item in imageIndex) {
if (item.imageSrc.contains('/countries/') && if (item.imageSrc.contains('/countries/') &&
item.imageSrc.contains(normalizedCountry)) { item.imageSrc.contains(normalizedCountry)) {
if (item.imageSrc.startsWith("/api/")) { if (item.imageSrc.startsWith('/api/')) {
_imageUrl = "https://twonly.eu/${item.imageSrc}"; _imageUrl = 'https://twonly.eu/${item.imageSrc}';
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
break; break;
@ -91,7 +93,7 @@ class _LocationFilterState extends State<LocationFilter> {
} }
if (location != null) { if (location != null) {
if (location!.county != "-") { if (location!.county != '-') {
return FilterSkeleton( return FilterSkeleton(
child: Positioned( child: Positioned(
bottom: 50, bottom: 50,
@ -108,35 +110,35 @@ class _LocationFilterState extends State<LocationFilter> {
} }
} }
return DateTimeFilter(color: Colors.black); return const DateTimeFilter(color: Colors.black);
} }
} }
class Sticker { class Sticker {
final String imageSrc;
final String source;
Sticker({required this.imageSrc, required this.source}); Sticker({required this.imageSrc, required this.source});
factory Sticker.fromJson(Map<String, dynamic> json) { factory Sticker.fromJson(Map<String, dynamic> json) {
return Sticker( return Sticker(
imageSrc: json['imageSrc'], imageSrc: json['imageSrc'] as String,
source: json['source'] ?? '', // Handle null source source: json['source'] as String? ?? '',
); );
} }
final String imageSrc;
final String source;
} }
Future<List<Sticker>> getStickerIndex() async { Future<List<Sticker>> getStickerIndex() async {
final directory = await getApplicationCacheDirectory(); final directory = await getApplicationCacheDirectory();
final indexFile = File('${directory.path}/stickers.json'); final indexFile = File('${directory.path}/stickers.json');
List<Sticker> res = []; var res = <Sticker>[];
if (await indexFile.exists() && !kDebugMode) { if (await indexFile.exists() && !kDebugMode) {
final lastModified = await indexFile.lastModified(); final lastModified = await indexFile.lastModified();
final difference = DateTime.now().difference(lastModified); final difference = DateTime.now().difference(lastModified);
final content = await indexFile.readAsString(); final content = await indexFile.readAsString();
List<dynamic> jsonList = json.decode(content); final jsonList = json.decode(content) as List;
res = jsonList.map((json) => Sticker.fromJson(json)).toList(); res = jsonList
.map((json) => Sticker.fromJson(json as Map<String, dynamic>))
.toList();
if (difference.inHours < 2) { if (difference.inHours < 2) {
return res; return res;
} }
@ -146,13 +148,15 @@ Future<List<Sticker>> getStickerIndex() async {
.get(Uri.parse('https://twonly.eu/api/sticker/stickers.json')); .get(Uri.parse('https://twonly.eu/api/sticker/stickers.json'));
if (response.statusCode == 200) { if (response.statusCode == 200) {
await indexFile.writeAsString(response.body); await indexFile.writeAsString(response.body);
List<dynamic> jsonList = json.decode(response.body); final jsonList = json.decode(response.body) as List;
return jsonList.map((json) => Sticker.fromJson(json)).toList(); return jsonList
.map((json) => Sticker.fromJson(json as Map<String, dynamic>))
.toList();
} else { } else {
return res; return res;
} }
} catch (e) { } catch (e) {
Log.error("$e"); Log.error('$e');
return res; return res;
} }
} }

View file

@ -111,7 +111,7 @@ class _TextViewState extends State<TextLayer> {
), ),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 20, fontSize: 20,
), ),
@ -182,7 +182,7 @@ class _TextViewState extends State<TextLayer> {
child: Text( child: Text(
widget.layerData.text, widget.layerData.text,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 20, fontSize: 20,
), ),

View file

@ -8,14 +8,13 @@ import 'package:twonly/src/views/camera/image_editor/layers/text_layer.dart';
/// View stacked layers (unbounded height, width) /// View stacked layers (unbounded height, width)
class LayersViewer extends StatelessWidget { class LayersViewer extends StatelessWidget {
final List<Layer> layers;
final Function()? onUpdate;
const LayersViewer({ const LayersViewer({
super.key,
required this.layers, required this.layers,
super.key,
this.onUpdate, this.onUpdate,
}); });
final List<Layer> layers;
final void Function()? onUpdate;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -19,7 +19,7 @@ class _EmojisState extends State<Emojis> {
initAsync(); initAsync();
} }
Future initAsync() async { Future<void> initAsync() async {
final user = await getUser(); final user = await getUser();
if (user == null) return; if (user == null) return;
setState(() { setState(() {
@ -28,7 +28,7 @@ class _EmojisState extends State<Emojis> {
}); });
} }
Future selectEmojis(String emoji) async { Future<void> selectEmojis(String emoji) async {
await updateUserdata((user) { await updateUserdata((user) {
if (user.lastUsedEditorEmojis == null) { if (user.lastUsedEditorEmojis == null) {
user.lastUsedEditorEmojis = [emoji]; user.lastUsedEditorEmojis = [emoji];

View file

@ -1,28 +1,31 @@
// ignore_for_file: strict_raw_type
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/headline.dart'; import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
class BestFriendsSelector extends StatelessWidget { class BestFriendsSelector extends StatelessWidget {
final List<Contact> users; const BestFriendsSelector({
final Function(int, bool) updateStatus;
final HashSet<int> selectedUserIds;
final bool isRealTwonly;
final String title;
const BestFriendsSelector(
{super.key,
required this.users, required this.users,
required this.isRealTwonly, required this.isRealTwonly,
required this.updateStatus, required this.updateStatus,
required this.selectedUserIds, required this.selectedUserIds,
required this.title}); required this.title,
super.key,
});
final List<Contact> users;
final void Function(int, bool) updateStatus;
final HashSet<int> selectedUserIds;
final bool isRealTwonly;
final String title;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -45,10 +48,11 @@ class BestFriendsSelector extends StatelessWidget {
} }
}, },
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 4), padding:
const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline.withAlpha(50), color: Theme.of(context).colorScheme.outline.withAlpha(50),
boxShadow: [ boxShadow: const [
BoxShadow( BoxShadow(
blurRadius: 10.9, blurRadius: 10.9,
color: Color.fromRGBO(0, 0, 0, 0.1), color: Color.fromRGBO(0, 0, 0, 0.1),
@ -57,7 +61,7 @@ class BestFriendsSelector extends StatelessWidget {
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
), ),
child: Text(context.lang.shareImagedSelectAll, child: Text(context.lang.shareImagedSelectAll,
style: TextStyle(fontSize: 10)), style: const TextStyle(fontSize: 10)),
), ),
), ),
], ],
@ -81,8 +85,8 @@ class BestFriendsSelector extends StatelessWidget {
isRealTwonly: isRealTwonly, isRealTwonly: isRealTwonly,
), ),
), ),
(secondUserIndex < users.length) if (secondUserIndex < users.length)
? Expanded( Expanded(
child: UserCheckbox( child: UserCheckbox(
isChecked: selectedUserIds isChecked: selectedUserIds
.contains(users[secondUserIndex].userId), .contains(users[secondUserIndex].userId),
@ -91,7 +95,8 @@ class BestFriendsSelector extends StatelessWidget {
isRealTwonly: isRealTwonly, isRealTwonly: isRealTwonly,
), ),
) )
: Expanded( else
Expanded(
child: Container(), child: Container(),
), ),
], ],
@ -105,35 +110,34 @@ class BestFriendsSelector extends StatelessWidget {
} }
class UserCheckbox extends StatelessWidget { class UserCheckbox extends StatelessWidget {
final Contact user;
final Function(int, bool) onChanged;
final bool isChecked;
final bool isRealTwonly;
const UserCheckbox({ const UserCheckbox({
super.key,
required this.user, required this.user,
required this.onChanged, required this.onChanged,
required this.isRealTwonly, required this.isRealTwonly,
required this.isChecked, required this.isChecked,
super.key,
}); });
final Contact user;
final void Function(int, bool) onChanged;
final bool isChecked;
final bool isRealTwonly;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String displayName = getContactDisplayName(user); final displayName = getContactDisplayName(user);
return Container( return Container(
padding: padding: const EdgeInsets.symmetric(
EdgeInsets.symmetric(horizontal: 3), // Padding inside the container horizontal: 3), // Padding inside the container
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
onChanged(user.userId, !isChecked); onChanged(user.userId, !isChecked);
}, },
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 0), padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline.withAlpha(50), color: Theme.of(context).colorScheme.outline.withAlpha(50),
boxShadow: [ boxShadow: const [
BoxShadow( BoxShadow(
blurRadius: 10.9, blurRadius: 10.9,
color: Color.fromRGBO(0, 0, 0, 0.1), color: Color.fromRGBO(0, 0, 0, 0.1),
@ -147,16 +151,15 @@ class UserCheckbox extends StatelessWidget {
contact: user, contact: user,
fontSize: 12, fontSize: 12,
), ),
SizedBox(width: 8), const SizedBox(width: 8),
Column( Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
if (isRealTwonly) if (isRealTwonly)
Padding( Padding(
padding: EdgeInsets.only(right: 2), padding: const EdgeInsets.only(right: 2),
child: VerifiedShield( child: VerifiedShield(
user, user,
size: 12, size: 12,
@ -186,10 +189,10 @@ class UserCheckbox extends StatelessWidget {
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
(Set states) { (Set states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return BorderSide(width: 0); return const BorderSide(width: 0);
} }
return BorderSide( return BorderSide(
width: 1, color: Theme.of(context).colorScheme.outline); color: Theme.of(context).colorScheme.outline);
}, },
), ),
onChanged: (bool? value) { onChanged: (bool? value) {

View file

@ -1,28 +1,29 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:io'; import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart' import 'package:screenshot/screenshot.dart';
show ErrorCode;
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart';
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/components/notification_badge.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/share_image_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart';
import 'package:flutter/services.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/camera/image_editor/data/image_item.dart'; import 'package:twonly/src/views/camera/image_editor/data/image_item.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart'; import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart';
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
import 'package:screenshot/screenshot.dart'; import 'package:twonly/src/views/camera/share_image_view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/components/notification_badge.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@ -34,13 +35,13 @@ const gMediaShowInfinite = 999999;
class ShareImageEditorView extends StatefulWidget { class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({ const ShareImageEditorView({
required this.mirrorVideo,
required this.useHighQuality,
required this.sharedFromGallery,
super.key, super.key,
this.imageBytes, this.imageBytes,
this.sendTo, this.sendTo,
this.videoFilePath, this.videoFilePath,
required this.mirrorVideo,
required this.useHighQuality,
required this.sharedFromGallery,
}); });
final Future<Uint8List?>? imageBytes; final Future<Uint8List?>? imageBytes;
final File? videoFilePath; final File? videoFilePath;
@ -87,9 +88,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
videoController?.initialize().then((_) { videoController?.initialize().then((_) {
videoController!.play(); videoController!.play();
setState(() {}); setState(() {});
}).catchError((Object error) { // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
Log.error(error); }).catchError(Log.error);
});
} }
} }
@ -103,7 +103,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
} }
Future initMediaFileUpload() async { Future<void> initMediaFileUpload() async {
// media init was already called... // media init was already called...
if (mediaUploadId != null) return; if (mediaUploadId != null) return;
@ -173,13 +173,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Icons.add_reaction_outlined, Icons.add_reaction_outlined,
tooltipText: context.lang.addEmoji, tooltipText: context.lang.addEmoji,
onPressed: () async { onPressed: () async {
EmojiLayerData? layer = await showModalBottomSheet( final layer = await showModalBottomSheet(
context: context, context: context,
backgroundColor: Colors.black, backgroundColor: Colors.black,
builder: (BuildContext context) { builder: (BuildContext context) {
return const Emojis(); return const Emojis();
}, },
); ) as Layer?;
if (layer == null) return; if (layer == null) return;
undoLayers.clear(); undoLayers.clear();
removedLayers.clear(); removedLayers.clear();
@ -190,9 +190,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
const SizedBox(height: 8), const SizedBox(height: 8),
NotificationBadge( NotificationBadge(
count: (widget.videoFilePath != null) count: (widget.videoFilePath != null)
? "0" ? '0'
: maxShowTime == 999999 : maxShowTime == 999999
? "" ? ''
: maxShowTime.toString(), : maxShowTime.toString(),
child: ActionButton( child: ActionButton(
(widget.videoFilePath != null) (widget.videoFilePath != null)
@ -239,8 +239,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onPressed: () async { onPressed: () async {
if (widget.sendTo != null) { if (widget.sendTo != null) {
if (!widget.sendTo!.verified) { if (!widget.sendTo!.verified) {
showAlertDialog(context, context.lang.shareImageUserNotVerified, await showAlertDialog(
context.lang.shareImageUserNotVerifiedDesc); context,
context.lang.shareImageUserNotVerified,
context.lang.shareImageUserNotVerifiedDesc,
);
return; return;
} }
} }
@ -277,9 +280,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
disable: layers.where((x) => !x.isDeleted).length <= 2, disable: layers.where((x) => !x.isDeleted).length <= 2,
onPressed: () { onPressed: () {
if (removedLayers.isNotEmpty) { if (removedLayers.isNotEmpty) {
var lastLayer = removedLayers.removeLast(); final lastLayer = removedLayers.removeLast()
lastLayer.isDeleted = false; ..isDeleted = false
lastLayer.isEditing = false; ..isEditing = false;
layers.add(lastLayer); layers.add(lastLayer);
setState(() {}); setState(() {});
return; return;
@ -308,15 +311,15 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
]; ];
} }
Future pushShareImageView() async { Future<void> pushShareImageView() async {
if (mediaUploadId == null) { if (mediaUploadId == null) {
await initMediaFileUpload(); await initMediaFileUpload();
if (mediaUploadId == null) return; if (mediaUploadId == null) return;
} }
Future<Uint8List?> imageBytes = getMergedImage(); final imageBytes = getMergedImage();
videoController?.pause(); await videoController?.pause();
if (isDisposed || !mounted) return; if (isDisposed || !mounted) return;
bool? wasSend = await Navigator.push( final wasSend = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ShareImageView( builder: (context) => ShareImageView(
@ -330,12 +333,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
mirrorVideo: widget.mirrorVideo, mirrorVideo: widget.mirrorVideo,
), ),
), ),
); ) as bool?;
if (wasSend != null && wasSend && mounted) { if (wasSend != null && wasSend && mounted) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.pop(context, true); Navigator.pop(context, true);
} else { } else {
videoController?.play(); await videoController?.play();
} }
} }
@ -343,13 +346,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Uint8List? image; Uint8List? image;
if (layers.length > 1 || widget.videoFilePath != null) { if (layers.length > 1 || widget.videoFilePath != null) {
for (var x in layers) { for (final x in layers) {
x.showCustomButtons = false; x.showCustomButtons = false;
} }
setState(() {}); setState(() {});
image = await screenshotController.capture( image = await screenshotController.capture(
pixelRatio: (widget.useHighQuality) ? pixelRatio : 1); pixelRatio: (widget.useHighQuality) ? pixelRatio : 1);
for (var x in layers) { for (final x in layers) {
x.showCustomButtons = true; x.showCustomButtons = true;
} }
setState(() {}); setState(() {});
@ -362,7 +365,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
Future<void> loadImage(Future<Uint8List?> imageFile) async { Future<void> loadImage(Future<Uint8List?> imageFile) async {
Uint8List? imageBytes = await imageFile; final imageBytes = await imageFile;
await currentImage.load(imageBytes); await currentImage.load(imageBytes);
if (isDisposed) return; if (isDisposed) return;
@ -380,19 +383,19 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}); });
} }
Future sendImageToSinglePerson() async { Future<void> sendImageToSinglePerson() async {
if (sendingOrLoadingImage) return; if (sendingOrLoadingImage) return;
setState(() { setState(() {
sendingOrLoadingImage = true; sendingOrLoadingImage = true;
}); });
Uint8List? imageBytes = await getMergedImage(); final imageBytes = await getMergedImage();
if (!context.mounted) return; if (!context.mounted) return;
if (imageBytes == null) { if (imageBytes == null) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.pop(context, true); Navigator.pop(context, true);
return; return;
} }
ErrorCode? err = await isAllowedToSend(); final err = await isAllowedToSend();
if (!context.mounted) return; if (!context.mounted) return;
if (err != null) { if (err != null) {
@ -407,8 +410,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
})); }));
} }
} else { } else {
Future imageHandler = final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes);
addOrModifyImageToUpload(mediaUploadId!, imageBytes);
// first finalize the upload // first finalize the upload
await finalizeUpload( await finalizeUpload(
@ -421,7 +423,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
); );
/// then call the upload process in the background /// then call the upload process in the background
encryptMediaFiles( await encryptMediaFiles(
mediaUploadId!, mediaUploadId!,
imageHandler, imageHandler,
videoUploadHandler, videoUploadHandler,
@ -526,7 +528,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
), ),
], ],
), ),
bottomNavigationBar: Container( bottomNavigationBar: ColoredBox(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: SafeArea( child: SafeArea(
child: Padding( child: Padding(
@ -541,7 +543,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
displayButtonLabel: widget.sendTo == null, displayButtonLabel: widget.sendTo == null,
isLoading: loadingImage, isLoading: loadingImage,
), ),
if (widget.sendTo != null) SizedBox(width: 10), if (widget.sendTo != null) const SizedBox(width: 10),
if (widget.sendTo != null) if (widget.sendTo != null)
OutlinedButton( OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
@ -549,7 +551,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
foregroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Theme.of(context).colorScheme.primary,
), ),
onPressed: pushShareImageView, onPressed: pushShareImageView,
child: FaIcon(FontAwesomeIcons.userPlus), child: const FaIcon(FontAwesomeIcons.userPlus),
), ),
SizedBox(width: widget.sendTo == null ? 20 : 10), SizedBox(width: widget.sendTo == null ? 20 : 10),
FilledButton.icon( FilledButton.icon(
@ -562,22 +564,22 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
color: Theme.of(context).colorScheme.inversePrimary, color: Theme.of(context).colorScheme.inversePrimary,
), ),
) )
: FaIcon(FontAwesomeIcons.solidPaperPlane), : const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
if (sendingOrLoadingImage) return; if (sendingOrLoadingImage) return;
if (widget.sendTo == null) return pushShareImageView(); if (widget.sendTo == null) return pushShareImageView();
sendImageToSinglePerson(); await sendImageToSinglePerson();
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30), const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
), ),
), ),
label: Text( label: Text(
(widget.sendTo == null) (widget.sendTo == null)
? context.lang.shareImagedEditorShareWith ? context.lang.shareImagedEditorShareWith
: getContactDisplayName(widget.sendTo!), : getContactDisplayName(widget.sendTo!),
style: TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
), ),
), ),
], ],

View file

@ -1,24 +1,25 @@
// ignore_for_file: strict_raw_type
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/headline.dart'; import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
class ShareImageView extends StatefulWidget { class ShareImageView extends StatefulWidget {
const ShareImageView({ const ShareImageView({
super.key,
required this.imageBytesFuture, required this.imageBytesFuture,
required this.isRealTwonly, required this.isRealTwonly,
required this.mirrorVideo, required this.mirrorVideo,
@ -27,6 +28,7 @@ class ShareImageView extends StatefulWidget {
required this.updateStatus, required this.updateStatus,
required this.videoUploadHandler, required this.videoUploadHandler,
required this.mediaUploadId, required this.mediaUploadId,
super.key,
this.enableVideoAudio, this.enableVideoAudio,
}); });
final Future<Uint8List?> imageBytesFuture; final Future<Uint8List?> imageBytesFuture;
@ -36,7 +38,7 @@ class ShareImageView extends StatefulWidget {
final HashSet<int> selectedUserIds; final HashSet<int> selectedUserIds;
final bool? enableVideoAudio; final bool? enableVideoAudio;
final int mediaUploadId; final int mediaUploadId;
final Function(int, bool) updateStatus; final void Function(int, bool) updateStatus;
final Future<bool>? videoUploadHandler; final Future<bool>? videoUploadHandler;
@override @override
@ -60,8 +62,7 @@ class _ShareImageView extends State<ShareImageView> {
void initState() { void initState() {
super.initState(); super.initState();
Stream<List<Contact>> allContacts = final allContacts = twonlyDB.contactsDao.watchContactsForShareView();
twonlyDB.contactsDao.watchContactsForShareView();
contactSub = allContacts.listen((allContacts) { contactSub = allContacts.listen((allContacts) {
setState(() { setState(() {
@ -73,13 +74,13 @@ class _ShareImageView extends State<ShareImageView> {
initAsync(); initAsync();
} }
Future initAsync() async { Future<void> initAsync() async {
imageBytes = await widget.imageBytesFuture; imageBytes = await widget.imageBytesFuture;
if (imageBytes != null) { if (imageBytes != null) {
final imageHandler = final imageHandler =
addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!); addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!);
// start with the pre upload of the media file... // start with the pre upload of the media file...
encryptMediaFiles( await encryptMediaFiles(
widget.mediaUploadId, imageHandler, widget.videoUploadHandler); widget.mediaUploadId, imageHandler, widget.videoUploadHandler);
} }
setState(() {}); setState(() {});
@ -91,12 +92,12 @@ class _ShareImageView extends State<ShareImageView> {
contactSub.cancel(); contactSub.cancel();
} }
Future updateUsers(List<Contact> users) async { Future<void> updateUsers(List<Contact> users) async {
// Sort contacts by flameCounter and then by totalMediaCounter // Sort contacts by flameCounter and then by totalMediaCounter
users.sort((a, b) { users.sort((a, b) {
// First, compare by flameCounter // First, compare by flameCounter
int flameComparison = (getFlameCounterFromContact(b)) final flameComparison = getFlameCounterFromContact(b)
.compareTo((getFlameCounterFromContact(a))); .compareTo(getFlameCounterFromContact(a));
if (flameComparison != 0) { if (flameComparison != 0) {
return flameComparison; // Sort by flameCounter in descending order return flameComparison; // Sort by flameCounter in descending order
} }
@ -106,11 +107,11 @@ class _ShareImageView extends State<ShareImageView> {
}); });
// Separate best friends and other users // Separate best friends and other users
List<Contact> bestFriends = []; final bestFriends = <Contact>[];
List<Contact> otherUsers = []; final otherUsers = <Contact>[];
List<Contact> pinnedContacts = users.where((c) => c.pinned).toList(); final pinnedContacts = users.where((c) => c.pinned).toList();
for (var contact in users) { for (final contact in users) {
if (contact.pinned) continue; if (contact.pinned) continue;
if (!contact.archived && if (!contact.archived &&
(getFlameCounterFromContact(contact)) > 0 && (getFlameCounterFromContact(contact)) > 0 &&
@ -128,10 +129,10 @@ class _ShareImageView extends State<ShareImageView> {
}); });
} }
Future _filterUsers(String query) async { Future<void> _filterUsers(String query) async {
lastQuery = query; lastQuery = query;
if (query.isEmpty) { if (query.isEmpty) {
updateUsers(contacts await updateUsers(contacts
.where((x) => .where((x) =>
!x.archived || !x.archived ||
!hideArchivedUsers || !hideArchivedUsers ||
@ -139,17 +140,17 @@ class _ShareImageView extends State<ShareImageView> {
.toList()); .toList());
return; return;
} }
List<Contact> usersFiltered = contacts final usersFiltered = contacts
.where((user) => getContactDisplayName(user) .where((user) => getContactDisplayName(user)
.toLowerCase() .toLowerCase()
.contains(query.toLowerCase())) .contains(query.toLowerCase()))
.toList(); .toList();
updateUsers(usersFiltered); await updateUsers(usersFiltered);
} }
void updateStatus(int userId, bool checked) { void updateStatus(int userId, bool checked) {
if (widget.isRealTwonly) { if (widget.isRealTwonly) {
Contact user = contacts.firstWhere((x) => x.userId == userId); final user = contacts.firstWhere((x) => x.userId == userId);
if (!user.verified) { if (!user.verified) {
showRealTwonlyWarning = true; showRealTwonlyWarning = true;
setState(() {}); setState(() {});
@ -169,18 +170,19 @@ class _ShareImageView extends State<ShareImageView> {
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), padding:
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
child: Column( child: Column(
children: [ children: [
if (showRealTwonlyWarning) if (showRealTwonlyWarning)
Text( Text(
context.lang.shareImageAllTwonlyWarning, context.lang.shareImageAllTwonlyWarning,
style: TextStyle(color: Colors.orange, fontSize: 13), style: const TextStyle(color: Colors.orange, fontSize: 13),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
if (showRealTwonlyWarning) const SizedBox(height: 10), if (showRealTwonlyWarning) const SizedBox(height: 10),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField( child: TextField(
onChanged: _filterUsers, onChanged: _filterUsers,
decoration: getInputDecoration( decoration: getInputDecoration(
@ -216,7 +218,7 @@ class _ShareImageView extends State<ShareImageView> {
children: [ children: [
Text( Text(
context.lang.shareImageShowArchived, context.lang.shareImageShowArchived,
style: TextStyle(fontSize: 10), style: const TextStyle(fontSize: 10),
), ),
Transform.scale( Transform.scale(
scale: 0.75, scale: 0.75,
@ -225,10 +227,9 @@ class _ShareImageView extends State<ShareImageView> {
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
(Set states) { (Set states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return BorderSide(width: 0); return const BorderSide(width: 0);
} }
return BorderSide( return BorderSide(
width: 1,
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.outline); .outline);
@ -261,7 +262,7 @@ class _ShareImageView extends State<ShareImageView> {
floatingActionButton: SizedBox( floatingActionButton: SizedBox(
height: 120, height: 120,
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -275,13 +276,13 @@ class _ShareImageView extends State<ShareImageView> {
color: Theme.of(context).colorScheme.inversePrimary, color: Theme.of(context).colorScheme.inversePrimary,
), ),
) )
: FaIcon(FontAwesomeIcons.solidPaperPlane), : const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
if (imageBytes == null || widget.selectedUserIds.isEmpty) { if (imageBytes == null || widget.selectedUserIds.isEmpty) {
return; return;
} }
ErrorCode? err = await isAllowedToSend(); final err = await isAllowedToSend();
if (!context.mounted) return; if (!context.mounted) return;
if (err != null) { if (err != null) {
@ -306,7 +307,7 @@ class _ShareImageView extends State<ShareImageView> {
); );
/// trigger the upload of the media file. /// trigger the upload of the media file.
handleNextMediaUploadSteps(widget.mediaUploadId); unawaited(handleNextMediaUploadSteps(widget.mediaUploadId));
if (context.mounted) { if (context.mounted) {
Navigator.pop(context, true); Navigator.pop(context, true);
@ -321,7 +322,7 @@ class _ShareImageView extends State<ShareImageView> {
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30), const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
), ),
backgroundColor: WidgetStateProperty.all<Color>( backgroundColor: WidgetStateProperty.all<Color>(
imageBytes == null || widget.selectedUserIds.isEmpty imageBytes == null || widget.selectedUserIds.isEmpty
@ -330,7 +331,7 @@ class _ShareImageView extends State<ShareImageView> {
)), )),
label: Text( label: Text(
context.lang.shareImagedEditorSendImage, context.lang.shareImagedEditorSendImage,
style: TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
), ),
), ),
], ],
@ -344,12 +345,12 @@ class _ShareImageView extends State<ShareImageView> {
class UserList extends StatelessWidget { class UserList extends StatelessWidget {
const UserList( const UserList(
this.users, { this.users, {
super.key,
required this.selectedUserIds, required this.selectedUserIds,
required this.updateStatus, required this.updateStatus,
required this.isRealTwonly, required this.isRealTwonly,
super.key,
}); });
final Function(int, bool) updateStatus; final void Function(int, bool) updateStatus;
final List<Contact> users; final List<Contact> users;
final bool isRealTwonly; final bool isRealTwonly;
final HashSet<int> selectedUserIds; final HashSet<int> selectedUserIds;
@ -364,12 +365,10 @@ class UserList extends StatelessWidget {
restorationId: 'new_message_users_list', restorationId: 'new_message_users_list',
itemCount: users.length, itemCount: users.length,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
Contact user = users[i]; final user = users[i];
int flameCounter = getFlameCounterFromContact(user); final flameCounter = getFlameCounterFromContact(user);
return ListTile( return ListTile(
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.start, // Center horizontally
crossAxisAlignment: CrossAxisAlignment.center, // Center vertically
children: [ children: [
if (isRealTwonly) if (isRealTwonly)
Padding( Padding(
@ -394,10 +393,9 @@ class UserList extends StatelessWidget {
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
(Set states) { (Set states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return BorderSide(width: 0); return const BorderSide(width: 0);
} }
return BorderSide( return BorderSide(color: Theme.of(context).colorScheme.outline);
width: 1, color: Theme.of(context).colorScheme.outline);
}, },
), ),
onChanged: (bool? value) { onChanged: (bool? value) {

View file

@ -1,25 +1,29 @@
// ignore_for_file: avoid_dynamic_calls
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/headline.dart'; import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
class AddNewUserView extends StatefulWidget { class AddNewUserView extends StatefulWidget {
@ -58,7 +62,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
}); });
} }
Future _addNewUser(BuildContext context) async { Future<void> _addNewUser(BuildContext context) async {
final user = await getUser(); final user = await getUser();
if (user == null || user.username == searchUserName.text) { if (user == null || user.username == searchUserName.text) {
return; return;
@ -84,23 +88,24 @@ class _SearchUsernameView extends State<AddNewUserView> {
return; return;
} }
int added = await twonlyDB.contactsDao.insertContact( final added = await twonlyDB.contactsDao.insertContact(
ContactsCompanion( ContactsCompanion(
username: Value(searchUserName.text), username: Value(searchUserName.text),
userId: Value(res.value.userdata.userId.toInt()), userId: Value(res.value.userdata.userId.toInt() as int),
requested: Value(false), requested: const Value(false),
), ),
); );
if (added > 0) { if (added > 0) {
if (await createNewSignalSession(res.value.userdata)) { if (await createNewSignalSession(
res.value.userdata as Response_UserData)) {
// before notifying the other party, add // before notifying the other party, add
await setupNotificationWithUsers( await setupNotificationWithUsers(
forceContact: res.value.userdata.userId.toInt(), forceContact: res.value.userdata.userId.toInt() as int,
); );
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
res.value.userdata.userId.toInt(), res.value.userdata.userId.toInt() as int,
MessageJson( MessageJson(
kind: MessageKind.contactRequest, kind: MessageKind.contactRequest,
timestamp: DateTime.now(), timestamp: DateTime.now(),
@ -111,7 +116,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
} }
} }
} else { } else {
showAlertDialog(context, context.lang.searchUsernameNotFound, await showAlertDialog(context, context.lang.searchUsernameNotFound,
context.lang.searchUsernameNotFoundBody(searchUserName.text)); context.lang.searchUsernameNotFoundBody(searchUserName.text));
} }
setState(() { setState(() {
@ -119,59 +124,59 @@ class _SearchUsernameView extends State<AddNewUserView> {
}); });
} }
InputDecoration getInputDecoration(hintText) { InputDecoration getInputDecoration(String hintText) {
final primaryColor = final primaryColor =
Theme.of(context).colorScheme.primary; // Get the primary color Theme.of(context).colorScheme.primary; // Get the primary color
return InputDecoration( return InputDecoration(
hintText: hintText, hintText: hintText,
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(9.0), borderRadius: BorderRadius.circular(9),
borderSide: BorderSide(color: primaryColor, width: 1.0), borderSide: BorderSide(color: primaryColor),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide( borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
color: Theme.of(context).colorScheme.outline, width: 1.0),
), ),
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isPreview = context.read<CustomChangeProvider>().plan == "Preview"; final isPreview = context.read<CustomChangeProvider>().plan == 'Preview';
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.searchUsernameTitle), title: Text(context.lang.searchUsernameTitle),
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), padding:
const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
child: Column( child: Column(
children: [ children: [
if (isPreview) ...[ if (isPreview) ...[
Padding( Padding(
padding: EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Text( child: Text(
context.lang.searchUserNamePreview, context.lang.searchUserNamePreview,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
FilledButton.icon( FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.shieldHeart), icon: const FaIcon(FontAwesomeIcons.shieldHeart),
onPressed: () { onPressed: () {
Navigator.push(context, Navigator.push(context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return SubscriptionView(); return const SubscriptionView();
})); }));
}, },
label: Text(context.lang.selectSubscription), label: Text(context.lang.selectSubscription),
), ),
SizedBox(height: 30), const SizedBox(height: 30),
], ],
if (!isPreview) ...[ if (!isPreview) ...[
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField( child: TextField(
onSubmitted: (_) { onSubmitted: (_) {
_addNewUser(context); _addNewUser(context);
@ -184,7 +189,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
}, },
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(12), LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9A-Z]')), FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')),
], ],
controller: searchUserName, controller: searchUserName,
decoration: decoration:
@ -204,18 +209,18 @@ class _SearchUsernameView extends State<AddNewUserView> {
), ),
), ),
), ),
floatingActionButton: (isPreview) floatingActionButton: isPreview
? null ? null
: Padding( : Padding(
padding: const EdgeInsets.only(bottom: 30.0), padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton( child: FloatingActionButton(
foregroundColor: Colors.white, foregroundColor: Colors.white,
onPressed: () { onPressed: () {
if (!_isLoading) _addNewUser(context); if (!_isLoading) _addNewUser(context);
}, },
child: (_isLoading) child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: FaIcon(FontAwesomeIcons.magnifyingGlassPlus), : const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),
), ),
), ),
); );
@ -237,9 +242,9 @@ class _ContactsListViewState extends State<ContactsListView> {
Tooltip( Tooltip(
message: context.lang.searchUserNameArchiveUserTooltip, message: context.lang.searchUserNameArchiveUserTooltip,
child: IconButton( child: IconButton(
icon: FaIcon(FontAwesomeIcons.boxArchive, size: 15), icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15),
onPressed: () async { onPressed: () async {
final update = ContactsCompanion(archived: Value(true)); const update = ContactsCompanion(archived: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update); await twonlyDB.contactsDao.updateContact(contact.userId, update);
}, },
), ),
@ -253,10 +258,10 @@ class _ContactsListViewState extends State<ContactsListView> {
Tooltip( Tooltip(
message: context.lang.searchUserNameBlockUserTooltip, message: context.lang.searchUserNameBlockUserTooltip,
child: IconButton( child: IconButton(
icon: Icon(Icons.person_off_rounded, icon: const Icon(Icons.person_off_rounded,
color: const Color.fromARGB(164, 244, 67, 54)), color: Color.fromARGB(164, 244, 67, 54)),
onPressed: () async { onPressed: () async {
final update = ContactsCompanion(blocked: Value(true)); const update = ContactsCompanion(blocked: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update); await twonlyDB.contactsDao.updateContact(contact.userId, update);
}, },
), ),
@ -264,17 +269,17 @@ class _ContactsListViewState extends State<ContactsListView> {
Tooltip( Tooltip(
message: context.lang.searchUserNameRejectUserTooltip, message: context.lang.searchUserNameRejectUserTooltip,
child: IconButton( child: IconButton(
icon: Icon(Icons.close, color: Colors.red), icon: const Icon(Icons.close, color: Colors.red),
onPressed: () async { onPressed: () async {
rejectUser(contact.userId); await rejectUser(contact.userId);
await deleteContact(contact.userId); await deleteContact(contact.userId);
}, },
), ),
), ),
IconButton( IconButton(
icon: Icon(Icons.check, color: Colors.green), icon: const Icon(Icons.check, color: Colors.green),
onPressed: () async { onPressed: () async {
final update = ContactsCompanion(accepted: Value(true)); const update = ContactsCompanion(accepted: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update); await twonlyDB.contactsDao.updateContact(contact.userId, update);
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
@ -286,7 +291,7 @@ class _ContactsListViewState extends State<ContactsListView> {
), ),
pushNotification: PushNotification(kind: PushKind.acceptRequest), pushNotification: PushNotification(kind: PushKind.acceptRequest),
); );
notifyContactsAboutProfileChange(); await notifyContactsAboutProfileChange();
}, },
), ),
]; ];

View file

@ -1,30 +1,31 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart'; import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart';
import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart';
import 'package:twonly/src/views/chats/chat_list_components/demo_user.card.dart'; import 'package:twonly/src/views/chats/chat_list_components/demo_user.card.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/chats/media_viewer.view.dart';
import 'package:twonly/src/views/chats/start_new_chat.view.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/message_send_state_icon.dart'; import 'package:twonly/src/views/components/message_send_state_icon.dart';
import 'package:twonly/src/views/components/notification_badge.dart'; import 'package:twonly/src/views/components/notification_badge.dart';
import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/components/user_context_menu.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/chats/media_viewer.view.dart';
import 'package:twonly/src/views/chats/start_new_chat.view.dart';
import 'package:twonly/src/views/settings/help/contact_us.view.dart'; import 'package:twonly/src/views/settings/help/contact_us.view.dart';
import 'package:twonly/src/views/settings/settings_main.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart';
@ -50,7 +51,7 @@ class _ChatListViewState extends State<ChatListView> {
super.initState(); super.initState();
} }
Future initAsync() async { Future<void> initAsync() async {
final stream = twonlyDB.contactsDao.watchContactsForChatList(); final stream = twonlyDB.contactsDao.watchContactsForChatList();
_contactsSub = stream.listen((contacts) { _contactsSub = stream.listen((contacts) {
setState(() { setState(() {
@ -59,7 +60,7 @@ class _ChatListViewState extends State<ChatListView> {
}); });
}); });
tutorial = Timer(Duration(seconds: 1), () async { tutorial = Timer(const Duration(seconds: 1), () async {
tutorial = null; tutorial = null;
if (!mounted) return; if (!mounted) return;
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers); await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
@ -86,15 +87,15 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isConnected = context.watch<CustomChangeProvider>().isConnected; final isConnected = context.watch<CustomChangeProvider>().isConnected;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row(children: [ title: Row(children: [
Text("twonly "), const Text('twonly '),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) { Navigator.push(context, MaterialPageRoute(builder: (context) {
return SubscriptionView(); return const SubscriptionView();
})); }));
}, },
child: Container( child: Container(
@ -102,7 +103,7 @@ class _ChatListViewState extends State<ChatListView> {
color: context.color.primary, color: context.color.primary,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
child: Text( child: Text(
context.watch<CustomChangeProvider>().plan, context.watch<CustomChangeProvider>().plan,
style: TextStyle( style: TextStyle(
@ -121,13 +122,13 @@ class _ChatListViewState extends State<ChatListView> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ContactUsView(), builder: (context) => const ContactUsView(),
), ),
); );
}, },
color: Colors.grey, color: Colors.grey,
tooltip: context.lang.feedbackTooltip, tooltip: context.lang.feedbackTooltip,
icon: FaIcon(FontAwesomeIcons.commentDots, size: 19), icon: const FaIcon(FontAwesomeIcons.commentDots, size: 19),
), ),
StreamBuilder( StreamBuilder(
stream: twonlyDB.contactsDao.watchContactsRequested(), stream: twonlyDB.contactsDao.watchContactsRequested(),
@ -140,12 +141,12 @@ class _ChatListViewState extends State<ChatListView> {
count: count.toString(), count: count.toString(),
child: IconButton( child: IconButton(
key: searchForOtherUsers, key: searchForOtherUsers,
icon: FaIcon(FontAwesomeIcons.userPlus, size: 18), icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => AddNewUserView(), builder: (context) => const AddNewUserView(),
), ),
); );
}, },
@ -158,11 +159,11 @@ class _ChatListViewState extends State<ChatListView> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SettingsMainView(), builder: (context) => const SettingsMainView(),
), ),
); );
}, },
icon: FaIcon(FontAwesomeIcons.gear, size: 19), icon: const FaIcon(FontAwesomeIcons.gear, size: 19),
) )
], ],
), ),
@ -172,7 +173,7 @@ class _ChatListViewState extends State<ChatListView> {
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: isConnected ? Container() : ConnectionInfo(), child: isConnected ? Container() : const ConnectionInfo(),
), ),
Positioned.fill( Positioned.fill(
child: (_contacts.isEmpty && _pinnedContacts.isEmpty) child: (_contacts.isEmpty && _pinnedContacts.isEmpty)
@ -180,12 +181,12 @@ class _ChatListViewState extends State<ChatListView> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: OutlinedButton.icon( child: OutlinedButton.icon(
icon: Icon(Icons.person_add), icon: const Icon(Icons.person_add),
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => AddNewUserView(), builder: (context) => const AddNewUserView(),
), ),
); );
}, },
@ -197,7 +198,7 @@ class _ChatListViewState extends State<ChatListView> {
onRefresh: () async { onRefresh: () async {
await apiService.close(() {}); await apiService.close(() {});
await apiService.connect(force: true); await apiService.connect(force: true);
await Future.delayed(Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
}, },
child: ListView.builder( child: ListView.builder(
itemCount: _pinnedContacts.length + itemCount: _pinnedContacts.length +
@ -207,12 +208,12 @@ class _ChatListViewState extends State<ChatListView> {
1, 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
return BackupNoticeCard(); return const BackupNoticeCard();
} }
index -= 1; index -= 1;
if (gIsDemoUser) { if (gIsDemoUser) {
if (index == 0) { if (index == 0) {
return DemoUserCard(); return const DemoUserCard();
} }
index -= 1; index -= 1;
} }
@ -230,9 +231,9 @@ class _ChatListViewState extends State<ChatListView> {
} }
// If there are pinned users, account for the Divider // If there are pinned users, account for the Divider
int adjustedIndex = index - _pinnedContacts.length; var adjustedIndex = index - _pinnedContacts.length;
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) { if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) {
return Divider(); return const Divider();
} }
// Adjust the index for the contacts list // Adjust the index for the contacts list
@ -255,18 +256,18 @@ class _ChatListViewState extends State<ChatListView> {
], ],
), ),
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30.0), padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton( child: FloatingActionButton(
foregroundColor: Colors.white, foregroundColor: Colors.white,
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return StartNewChatView(); return const StartNewChatView();
}), }),
); );
}, },
child: FaIcon(FontAwesomeIcons.penToSquare), child: const FaIcon(FontAwesomeIcons.penToSquare),
), ),
), ),
); );
@ -274,12 +275,11 @@ class _ChatListViewState extends State<ChatListView> {
} }
class UserListItem extends StatefulWidget { class UserListItem extends StatefulWidget {
const UserListItem(
{required this.user, required this.firstUserListItemKey, super.key});
final Contact user; final Contact user;
final GlobalKey? firstUserListItemKey; final GlobalKey? firstUserListItemKey;
const UserListItem(
{super.key, required this.user, required this.firstUserListItemKey});
@override @override
State<UserListItem> createState() => _UserListItem(); State<UserListItem> createState() => _UserListItem();
} }
@ -363,11 +363,11 @@ class _UserListItem extends State<UserListItem> {
void lastUpdateTime() { void lastUpdateTime() {
// Change the color every 200 milliseconds // Change the color every 200 milliseconds
updateTime = Timer.periodic(Duration(milliseconds: 200), (timer) { updateTime = Timer.periodic(const Duration(milliseconds: 200), (timer) {
setState(() { setState(() {
if (currentMessage != null) { if (currentMessage != null) {
lastMessageInSeconds = lastMessageInSeconds =
(DateTime.now().difference(currentMessage!.sendAt)).inSeconds; DateTime.now().difference(currentMessage!.sendAt).inSeconds;
if (lastMessageInSeconds < 0) { if (lastMessageInSeconds < 0) {
lastMessageInSeconds = 0; lastMessageInSeconds = 0;
} }
@ -378,7 +378,7 @@ class _UserListItem extends State<UserListItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int flameCounter = getFlameCounterFromContact(widget.user); final flameCounter = getFlameCounterFromContact(widget.user);
return Stack( return Stack(
children: [ children: [
@ -405,11 +405,11 @@ class _UserListItem extends State<UserListItem> {
: Row( : Row(
children: [ children: [
MessageSendStateIcon(previewMessages), MessageSendStateIcon(previewMessages),
Text(""), const Text(''),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
formatDuration(lastMessageInSeconds), formatDuration(lastMessageInSeconds),
style: TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
if (flameCounter > 0) if (flameCounter > 0)
FlameCounterWidget( FlameCounterWidget(
@ -442,7 +442,7 @@ class _UserListItem extends State<UserListItem> {
)); ));
return; return;
} }
List<Message> msgs = previewMessages final msgs = previewMessages
.where((x) => x.kind == MessageKind.media) .where((x) => x.kind == MessageKind.media)
.toList(); .toList();
if (msgs.isNotEmpty && if (msgs.isNotEmpty &&
@ -452,6 +452,7 @@ class _UserListItem extends State<UserListItem> {
switch (msgs.first.downloadState) { switch (msgs.first.downloadState) {
case DownloadState.pending: case DownloadState.pending:
startDownloadMedia(msgs.first, true); startDownloadMedia(msgs.first, true);
return;
case DownloadState.downloaded: case DownloadState.downloaded:
Navigator.push( Navigator.push(
context, context,
@ -459,9 +460,10 @@ class _UserListItem extends State<UserListItem> {
return MediaViewerView(widget.user); return MediaViewerView(widget.user);
}), }),
); );
default:
}
return; return;
case DownloadState.downloading:
return;
}
} }
Navigator.push( Navigator.push(
context, context,

View file

@ -20,7 +20,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
super.initState(); super.initState();
} }
Future initAsync() async { Future<void> initAsync() async {
final user = await getUser(); final user = await getUser();
showBackupNotice = false; showBackupNotice = false;
if (user != null && if (user != null &&
@ -47,7 +47,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
children: [ children: [
Text( Text(
context.lang.backupNoticeTitle, context.lang.backupNoticeTitle,
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -55,7 +55,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
SizedBox(height: 5), SizedBox(height: 5),
Text( Text(
context.lang.backupNoticeDesc, context.lang.backupNoticeDesc,
style: TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,

View file

@ -15,7 +15,7 @@ class DemoUserCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
"This is a Demo-Preview.", 'This is a Demo-Preview.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: !isDarkMode(context) ? Colors.white : Colors.black, color: !isDarkMode(context) ? Colors.white : Colors.black,
@ -25,12 +25,12 @@ class DemoUserCard extends StatelessWidget {
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
await deleteLocalUserData(); await deleteLocalUserData();
Restart.restartApp( await Restart.restartApp(
notificationTitle: 'Demo-Mode exited.', notificationTitle: 'Demo-Mode exited.',
notificationBody: 'Click here to open the app again', notificationBody: 'Click here to open the app again',
); );
}, },
child: Text("Register"), child: const Text('Register'),
) )
], ],
), ),

View file

@ -2,33 +2,33 @@ import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/components/user_context_menu.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart';
Color getMessageColor(Message message) { Color getMessageColor(Message message) {
return (message.messageOtherId == null) return (message.messageOtherId == null)
? Color.fromARGB(255, 58, 136, 102) ? const Color.fromARGB(255, 58, 136, 102)
: Color.fromARGB(83, 68, 137, 255); : const Color.fromARGB(83, 68, 137, 255);
} }
/// Displays detailed information about a SampleItem. /// Displays detailed information about a SampleItem.
@ -45,7 +45,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
TextEditingController newMessageController = TextEditingController(); TextEditingController newMessageController = TextEditingController();
HashSet<int> alreadyReportedOpened = HashSet<int>(); HashSet<int> alreadyReportedOpened = HashSet<int>();
late Contact user; late Contact user;
String currentInputText = ""; String currentInputText = '';
late StreamSubscription<Contact?> userSub; late StreamSubscription<Contact?> userSub;
late StreamSubscription<List<Message>> messageSub; late StreamSubscription<List<Message>> messageSub;
List<Message> messages = []; List<Message> messages = [];
@ -64,7 +64,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
textFieldFocus = FocusNode(); textFieldFocus = FocusNode();
initStreams(); initStreams();
tutorial = Timer(Duration(seconds: 1), () async { tutorial = Timer(const Duration(seconds: 1), () async {
tutorial = null; tutorial = null;
if (!mounted) return; if (!mounted) return;
await showVerifyShieldTutorial(context, verifyShieldKey); await showVerifyShieldTutorial(context, verifyShieldKey);
@ -80,10 +80,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
textFieldFocus.dispose(); textFieldFocus.dispose();
} }
Future initStreams() async { Future<void> initStreams() async {
await twonlyDB.messagesDao.removeOldMessages(); await twonlyDB.messagesDao.removeOldMessages();
Stream<Contact?> contact = final contact = twonlyDB.contactsDao.watchContact(widget.contact.userId);
twonlyDB.contactsDao.watchContact(widget.contact.userId);
userSub = contact.listen((contact) { userSub = contact.listen((contact) {
if (contact == null) return; if (contact == null) return;
setState(() { setState(() {
@ -91,46 +90,48 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
}); });
}); });
Stream<List<Message>> msgStream = final msgStream =
twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId); twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId);
messageSub = msgStream.listen((msgs) async { messageSub = msgStream.listen((msgs) async {
// if (!context.mounted) return; // if (!context.mounted) return;
if (Platform.isAndroid) { if (Platform.isAndroid) {
flutterLocalNotificationsPlugin.cancel(widget.contact.userId); await flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
} else { } else {
flutterLocalNotificationsPlugin.cancelAll(); await flutterLocalNotificationsPlugin.cancelAll();
} }
List<Message> displayedMessages = []; final displayedMessages = <Message>[];
// should be cleared // should be cleared
Map<int, List<Message>> tmpTextReactionsToMessageId = {}; final tmpTextReactionsToMessageId = <int, List<Message>>{};
Map<int, List<Message>> tmpEmojiReactionsToMessageId = {}; final tmpEmojiReactionsToMessageId = <int, List<Message>>{};
List<int> openedMessageOtherIds = []; final openedMessageOtherIds = <int>[];
Map<int, int> messageOtherMessageIdToMyMessageId = {}; final messageOtherMessageIdToMyMessageId = <int, int>{};
/// there is probably a better way... /// there is probably a better way...
for (Message msg in msgs) { for (final msg in msgs) {
if (msg.messageOtherId != null) { if (msg.messageOtherId != null) {
messageOtherMessageIdToMyMessageId[msg.messageOtherId!] = messageOtherMessageIdToMyMessageId[msg.messageOtherId!] =
msg.messageId; msg.messageId;
} }
} }
for (Message msg in msgs) { for (final msg in msgs) {
if (msg.kind == MessageKind.textMessage && if (msg.kind == MessageKind.textMessage &&
msg.messageOtherId != null && msg.messageOtherId != null &&
msg.openedAt == null) { msg.openedAt == null) {
openedMessageOtherIds.add(msg.messageOtherId!); openedMessageOtherIds.add(msg.messageOtherId!);
} }
int? responseId = msg.responseToMessageId ?? final responseId = msg.responseToMessageId ??
messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId]; messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId];
if (responseId != null) { if (responseId != null) {
bool added = false; var added = false;
MessageContent? content = final content = MessageContent.fromJson(
MessageContent.fromJson(msg.kind, jsonDecode(msg.contentJson!)); msg.kind,
jsonDecode(msg.contentJson!) as Map,
);
if (content is TextMessageContent) { if (content is TextMessageContent) {
if (content.text.isNotEmpty && !isEmoji(content.text)) { if (content.text.isNotEmpty && !isEmoji(content.text)) {
added = true; added = true;
@ -175,8 +176,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
}); });
} }
Future _sendMessage() async { Future<void> _sendMessage() async {
if (newMessageController.text == "") return; if (newMessageController.text == '') return;
await sendTextMessage( await sendTextMessage(
user.userId, user.userId,
@ -197,7 +198,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
), ),
); );
newMessageController.clear(); newMessageController.clear();
currentInputText = ""; currentInputText = '';
responseToMessage = null; responseToMessage = null;
setState(() {}); setState(() {});
} }
@ -207,45 +208,44 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
if (message.kind == MessageKind.textMessage) { if (message.kind == MessageKind.textMessage) {
if (message.contentJson != null) { if (message.contentJson != null) {
MessageContent? content = MessageContent.fromJson( final content = MessageContent.fromJson(
MessageKind.textMessage, jsonDecode(message.contentJson!)); MessageKind.textMessage, jsonDecode(message.contentJson!) as Map);
if (content is TextMessageContent) { if (content is TextMessageContent) {
subtitle = truncateString(content.text); subtitle = truncateString(content.text);
} }
} }
} }
if (message.kind == MessageKind.media) { if (message.kind == MessageKind.media) {
MessageContent? content = MessageContent.fromJson( final content = MessageContent.fromJson(
MessageKind.media, jsonDecode(message.contentJson!)); MessageKind.media, jsonDecode(message.contentJson!) as Map);
if (content is MediaMessageContent) { if (content is MediaMessageContent) {
subtitle = content.isVideo ? "Video" : "Image"; subtitle = content.isVideo ? 'Video' : 'Image';
} }
} }
String username = "You"; var username = 'You';
if (message.messageOtherId != null) { if (message.messageOtherId != null) {
username = getContactDisplayName(widget.contact); username = getContactDisplayName(widget.contact);
} }
Color color = getMessageColor(message); final color = getMessageColor(message);
return Container( return Container(
padding: EdgeInsets.only(left: 10), padding: const EdgeInsets.only(left: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
left: BorderSide( left: BorderSide(
color: color, color: color,
width: 2.0, width: 2,
), ),
), ),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
username, username,
style: TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
if (subtitle != null) Text(subtitle) if (subtitle != null) Text(subtitle)
], ],
@ -269,14 +269,14 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
contact: user, contact: user,
fontSize: 19, fontSize: 19,
), ),
SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,
child: Row( child: Row(
children: [ children: [
Text(getContactDisplayName(user)), Text(getContactDisplayName(user)),
SizedBox(width: 10), const SizedBox(width: 10),
VerifiedShield(key: verifyShieldKey, user), VerifiedShield(key: verifyShieldKey, user),
], ],
), ),
@ -300,8 +300,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
index -= 1; index -= 1;
double size = 44; double size = 44;
if (messages[index].kind == MessageKind.textMessage) { if (messages[index].kind == MessageKind.textMessage) {
TextMessageContent? content = TextMessageContent.fromJson( final content = TextMessageContent.fromJson(
jsonDecode(messages[index].contentJson!)); jsonDecode(messages[index].contentJson!) as Map);
if (EmojiAnimation.supported(content.text)) { if (EmojiAnimation.supported(content.text)) {
size = 99; size = 99;
} else { } else {
@ -321,9 +321,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
if (reactions != null && reactions.isNotEmpty) { if (reactions != null && reactions.isNotEmpty) {
for (final reaction in reactions) { for (final reaction in reactions) {
if (reaction.kind == MessageKind.textMessage) { if (reaction.kind == MessageKind.textMessage) {
TextMessageContent? content = final content = TextMessageContent.fromJson(
TextMessageContent.fromJson( jsonDecode(reaction.contentJson!) as Map);
jsonDecode(reaction.contentJson!));
size += calculateNumberOfLines(content.text, size += calculateNumberOfLines(content.text,
MediaQuery.of(context).size.width * 0.5, 14) * MediaQuery.of(context).size.width * 0.5, 14) *
27; 27;
@ -362,7 +361,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
if (responseToMessage != null && !user.deleted) if (responseToMessage != null && !user.deleted)
Container( Container(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
bottom: 00,
left: 20, left: 20,
right: 20, right: 20,
top: 10, top: 10,
@ -376,7 +374,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
responseToMessage = null; responseToMessage = null;
}); });
}, },
icon: FaIcon( icon: const FaIcon(
FontAwesomeIcons.xmark, FontAwesomeIcons.xmark,
size: 16, size: 16,
), ),
@ -412,25 +410,23 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
decoration: inputTextMessageDeco(context), decoration: inputTextMessageDeco(context),
), ),
), ),
(currentInputText != "") if (currentInputText != '')
? IconButton( IconButton(
padding: EdgeInsets.all(15), padding: const EdgeInsets.all(15),
icon: icon: const FaIcon(
FaIcon(FontAwesomeIcons.solidPaperPlane), FontAwesomeIcons.solidPaperPlane),
onPressed: () { onPressed: _sendMessage,
_sendMessage();
},
) )
: IconButton( else
icon: FaIcon(FontAwesomeIcons.camera), IconButton(
padding: EdgeInsets.all(15), icon: const FaIcon(FontAwesomeIcons.camera),
padding: const EdgeInsets.all(15),
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return CameraSendToView( return CameraSendToView(widget.contact);
widget.contact);
}, },
), ),
); );
@ -469,7 +465,6 @@ double calculateNumberOfLines(String text, double width, double fontSize) {
style: TextStyle(fontSize: fontSize), style: TextStyle(fontSize: fontSize),
), ),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
); )..layout(maxWidth: width - 32);
textPainter.layout(maxWidth: (width - 32));
return textPainter.computeLineMetrics().length.toDouble(); return textPainter.computeLineMetrics().length.toDouble();
} }

View file

@ -41,7 +41,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
checkIfTutorialCanBeShown(); checkIfTutorialCanBeShown();
} }
Future checkIfTutorialCanBeShown() async { Future<void> checkIfTutorialCanBeShown() async {
if (widget.message.openedAt == null && if (widget.message.openedAt == null &&
widget.message.messageOtherId != null || widget.message.messageOtherId != null ||
widget.message.mediaStored) { widget.message.mediaStored) {

View file

@ -1,13 +1,14 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_response_columns.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_text_response_columns.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/memory_item.model.dart';
class ChatListEntry extends StatefulWidget { class ChatListEntry extends StatefulWidget {
const ChatListEntry( const ChatListEntry(
@ -17,8 +18,8 @@ class ChatListEntry extends StatefulWidget {
this.lastMessageFromSameUser, this.lastMessageFromSameUser,
this.textReactions, this.textReactions,
this.otherReactions, { this.otherReactions, {
super.key,
required this.onResponseTriggered, required this.onResponseTriggered,
super.key,
}); });
final Message message; final Message message;
final Contact contact; final Contact contact;
@ -26,7 +27,7 @@ class ChatListEntry extends StatefulWidget {
final List<Message> textReactions; final List<Message> textReactions;
final List<Message> otherReactions; final List<Message> otherReactions;
final List<MemoryItem> galleryItems; final List<MemoryItem> galleryItems;
final Function(Message) onResponseTriggered; final void Function(Message) onResponseTriggered;
@override @override
State<ChatListEntry> createState() => _ChatListEntryState(); State<ChatListEntry> createState() => _ChatListEntryState();
@ -40,7 +41,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
void initState() { void initState() {
super.initState(); super.initState();
final msgContent = MessageContent.fromJson( final msgContent = MessageContent.fromJson(
widget.message.kind, jsonDecode(widget.message.contentJson!)); widget.message.kind, jsonDecode(widget.message.contentJson!) as Map);
if (msgContent is TextMessageContent) { if (msgContent is TextMessageContent) {
textMessage = msgContent.text; textMessage = msgContent.text;
} }
@ -50,16 +51,14 @@ class _ChatListEntryState extends State<ChatListEntry> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (content == null) return Container(); if (content == null) return Container();
bool right = widget.message.messageOtherId == null; final right = widget.message.messageOtherId == null;
return Container( return Align(
// tag: "${widget.message.mediaUploadId ?? widget.message.messageId}",
child: Align(
alignment: right ? Alignment.centerRight : Alignment.centerLeft, alignment: right ? Alignment.centerRight : Alignment.centerLeft,
child: Padding( child: Padding(
padding: widget.lastMessageFromSameUser padding: widget.lastMessageFromSameUser
? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10) ? const EdgeInsets.only(top: 5, right: 10, left: 10)
: EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10), : const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
child: Column( child: Column(
mainAxisAlignment: mainAxisAlignment:
right ? MainAxisAlignment.end : MainAxisAlignment.start, right ? MainAxisAlignment.end : MainAxisAlignment.start,
@ -71,12 +70,13 @@ class _ChatListEntryState extends State<ChatListEntry> {
child: Stack( child: Stack(
alignment: right ? Alignment.centerRight : Alignment.centerLeft, alignment: right ? Alignment.centerRight : Alignment.centerLeft,
children: [ children: [
(textMessage != null) if (textMessage != null)
? ChatTextEntry( ChatTextEntry(
message: widget.message, message: widget.message,
text: textMessage!, text: textMessage!,
) )
: ChatMediaEntry( else
ChatMediaEntry(
message: widget.message, message: widget.message,
contact: widget.contact, contact: widget.contact,
galleryItems: widget.galleryItems, galleryItems: widget.galleryItems,
@ -104,6 +104,6 @@ class _ChatListEntryState extends State<ChatListEntry> {
], ],
), ),
), ),
)); );
} }
} }

View file

@ -1,15 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
class ReactionRow extends StatefulWidget { class ReactionRow extends StatefulWidget {
const ReactionRow({ const ReactionRow({
super.key,
required this.otherReactions, required this.otherReactions,
required this.message, required this.message,
super.key,
}); });
final List<Message> otherReactions; final List<Message> otherReactions;
@ -22,18 +23,18 @@ class ReactionRow extends StatefulWidget {
class _ReactionRowState extends State<ReactionRow> { class _ReactionRowState extends State<ReactionRow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> children = []; final children = <Widget>[];
bool hasOneTextReaction = false; var hasOneTextReaction = false;
bool hasOneReopened = false; var hasOneReopened = false;
for (final reaction in widget.otherReactions) { for (final reaction in widget.otherReactions) {
MessageContent? content = MessageContent.fromJson( final content = MessageContent.fromJson(
reaction.kind, jsonDecode(reaction.contentJson!)); reaction.kind, jsonDecode(reaction.contentJson!) as Map);
if (content is ReopenedMediaFileContent) { if (content is ReopenedMediaFileContent) {
if (hasOneReopened) continue; if (hasOneReopened) continue;
hasOneReopened = true; hasOneReopened = true;
children.add( children.add(
Expanded( const Expanded(
child: Align( child: Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: Padding( child: Padding(
@ -61,12 +62,12 @@ class _ReactionRowState extends State<ReactionRow> {
child: EmojiAnimation(emoji: content.text), child: EmojiAnimation(emoji: content.text),
); );
} else { } else {
child = Text(content.text, style: TextStyle(fontSize: 14)); child = Text(content.text, style: const TextStyle(fontSize: 14));
} }
children.insert( children.insert(
0, 0,
Padding( Padding(
padding: EdgeInsets.only(left: 3), padding: const EdgeInsets.only(left: 3),
child: child, child: child,
), ),
); );

View file

@ -1,15 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
class ChatTextResponseColumns extends StatelessWidget { class ChatTextResponseColumns extends StatelessWidget {
const ChatTextResponseColumns({ const ChatTextResponseColumns({
super.key,
required this.textReactions, required this.textReactions,
required this.right, required this.right,
super.key,
}); });
final List<Message> textReactions; final List<Message> textReactions;
@ -17,25 +18,25 @@ class ChatTextResponseColumns extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> children = []; final children = <Widget>[];
for (final reaction in textReactions) { for (final reaction in textReactions) {
MessageContent? content = MessageContent.fromJson( final content = MessageContent.fromJson(
reaction.kind, jsonDecode(reaction.contentJson!)); reaction.kind, jsonDecode(reaction.contentJson!) as Map);
if (content is TextMessageContent) { if (content is TextMessageContent) {
var entries = [ var entries = [
FaIcon( const FaIcon(
FontAwesomeIcons.reply, FontAwesomeIcons.reply,
size: 10, size: 10,
), ),
SizedBox(width: 5), const SizedBox(width: 5),
Container( Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.5, maxWidth: MediaQuery.of(context).size.width * 0.5,
), ),
child: Text( child: Text(
content.text, content.text,
style: TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
textAlign: right ? TextAlign.right : TextAlign.left, textAlign: right ? TextAlign.right : TextAlign.left,
)), )),
]; ];
@ -43,20 +44,21 @@ class ChatTextResponseColumns extends StatelessWidget {
entries = entries.reversed.toList(); entries = entries.reversed.toList();
} }
Color color = getMessageColor(reaction); final color = getMessageColor(reaction);
children.insert( children.insert(
0, 0,
Container( Container(
padding: EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10), padding:
const EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10),
child: Container( child: Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
), ),
padding: EdgeInsets.symmetric(vertical: 1, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: color,
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View file

@ -1,20 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/views/components/message_send_state_icon.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/views/components/message_send_state_icon.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
class InChatMediaViewer extends StatefulWidget { class InChatMediaViewer extends StatefulWidget {
const InChatMediaViewer({ const InChatMediaViewer({
super.key,
required this.message, required this.message,
required this.contact, required this.contact,
required this.color, required this.color,
required this.galleryItems, required this.galleryItems,
required this.canBeReopened, required this.canBeReopened,
super.key,
}); });
final Message message; final Message message;
@ -40,9 +41,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
initStream(); initStream();
} }
Future loadIndexAsync() async { Future<void> loadIndexAsync() async {
if (!widget.message.mediaStored) return; if (!widget.message.mediaStored) return;
_timer = Timer.periodic(Duration(milliseconds: 10), (timer) { _timer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
/// when the galleryItems are updated this widget is not reloaded /// when the galleryItems are updated this widget is not reloaded
/// so using this timer as a workaround /// so using this timer as a workaround
if (loadIndex()) { if (loadIndex()) {
@ -72,7 +73,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
// videoController?.dispose(); // videoController?.dispose();
} }
Future initStream() async { Future<void> initStream() async {
/// When the image is opened from the chat and then stored the /// When the image is opened from the chat and then stored the
/// image is not loaded so this will trigger an initAsync when mediaStored is changed /// image is not loaded so this will trigger an initAsync when mediaStored is changed
@ -85,22 +86,21 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
messageStream = stream.listen((updated) async { messageStream = stream.listen((updated) async {
if (updated != null) { if (updated != null) {
if (updated.mediaStored) { if (updated.mediaStored) {
messageStream?.cancel(); await messageStream?.cancel();
loadIndexAsync(); await loadIndexAsync();
} }
} }
}); });
} }
Future onTap() async { Future<void> onTap() async {
if (galleryItemIndex == null) return; if (galleryItemIndex == null) return;
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => MemoriesPhotoSliderView( builder: (context) => MemoriesPhotoSliderView(
galleryItems: widget.galleryItems, galleryItems: widget.galleryItems,
initialIndex: galleryItemIndex!, initialIndex: galleryItemIndex!,
scrollDirection: Axis.horizontal,
), ),
), ),
); );
@ -110,15 +110,14 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (galleryItemIndex == null) { if (galleryItemIndex == null) {
return Container( return Container(
constraints: BoxConstraints( constraints: const BoxConstraints(
minHeight: 39, minHeight: 39,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: widget.color, color: widget.color,
width: 1.0,
), ),
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
@ -135,10 +134,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Colors.transparent, color: Colors.transparent,
width: 1.0,
), ),
color: Colors.transparent, color: Colors.transparent,
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12),
), ),
child: MemoriesItemThumbnail( child: MemoriesItemThumbnail(
galleryItem: widget.galleryItems[galleryItemIndex!], galleryItem: widget.galleryItems[galleryItemIndex!],

View file

@ -1,3 +1,5 @@
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_function_invocation
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -15,23 +17,22 @@ import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
class MessageActions extends StatefulWidget { class MessageActions extends StatefulWidget {
final Widget child;
final Message message;
final VoidCallback onResponseTriggered;
const MessageActions({ const MessageActions({
super.key,
required this.child, required this.child,
required this.message, required this.message,
required this.onResponseTriggered, required this.onResponseTriggered,
super.key,
}); });
final Widget child;
final Message message;
final VoidCallback onResponseTriggered;
@override @override
State<MessageActions> createState() => _SlidingResponseWidgetState(); State<MessageActions> createState() => _SlidingResponseWidgetState();
} }
class _SlidingResponseWidgetState extends State<MessageActions> { class _SlidingResponseWidgetState extends State<MessageActions> {
double _offsetX = 0.0; double _offsetX = 0;
bool gotFeedback = false; bool gotFeedback = false;
void _onHorizontalDragUpdate(DragUpdateDetails details) { void _onHorizontalDragUpdate(DragUpdateDetails details) {
@ -77,7 +78,7 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
), ),
), ),
if (_offsetX >= 40) if (_offsetX >= 40)
Positioned( const Positioned(
left: 20, left: 20,
top: 0, top: 0,
bottom: 0, bottom: 0,
@ -98,16 +99,15 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
} }
class MessageContextMenu extends StatelessWidget { class MessageContextMenu extends StatelessWidget {
final Widget child;
final Message message;
final VoidCallback onResponseTriggered;
const MessageContextMenu({ const MessageContextMenu({
super.key,
required this.message, required this.message,
required this.child, required this.child,
required this.onResponseTriggered, required this.onResponseTriggered,
super.key,
}); });
final Widget child;
final Message message;
final VoidCallback onResponseTriggered;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -122,17 +122,17 @@ class MessageContextMenu extends StatelessWidget {
PieAction( PieAction(
tooltip: Text(context.lang.react), tooltip: Text(context.lang.react),
onSelect: () async { onSelect: () async {
EmojiLayerData? layer = await showModalBottomSheet( final layer = await showModalBottomSheet(
context: context, context: context,
backgroundColor: Colors.black, backgroundColor: Colors.black,
builder: (BuildContext context) { builder: (BuildContext context) {
return const Emojis(); return const Emojis();
}, },
); ) as TextLayerData?;
if (layer == null) return; if (layer == null) return;
Log.info(layer.text); Log.info(layer.text);
sendTextMessage( await sendTextMessage(
message.contactId, message.contactId,
TextMessageContent( TextMessageContent(
text: layer.text, text: layer.text,
@ -171,7 +171,7 @@ class MessageContextMenu extends StatelessWidget {
PieAction( PieAction(
tooltip: Text(context.lang.delete), tooltip: Text(context.lang.delete),
onSelect: () async { onSelect: () async {
bool delete = await showAlertDialog( final delete = await showAlertDialog(
context, context,
context.lang.deleteTitle, context.lang.deleteTitle,
null, null,

View file

@ -1,6 +1,9 @@
// ignore_for_file: inference_failure_on_collection_literal, avoid_dynamic_calls
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -8,28 +11,28 @@ import 'package:lottie/lottie.dart';
import 'package:no_screenshot/no_screenshot.dart'; import 'package:no_screenshot/no_screenshot.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
final _noScreenshot = NoScreenshot.instance; final NoScreenshot _noScreenshot = NoScreenshot.instance;
class MediaViewerView extends StatefulWidget { class MediaViewerView extends StatefulWidget {
final Contact contact;
const MediaViewerView(this.contact, {super.key, this.initialMessage}); const MediaViewerView(this.contact, {super.key, this.initialMessage});
final Contact contact;
final Message? initialMessage; final Message? initialMessage;
@ -89,17 +92,17 @@ class _MediaViewerViewState extends State<MediaViewerView> {
super.dispose(); super.dispose();
} }
Future asyncLoadNextMedia(bool firstRun) async { Future<void> asyncLoadNextMedia(bool firstRun) async {
Stream<List<Message>> messages = final messages =
twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId); twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId);
_subscription = messages.listen((messages) { _subscription = messages.listen((messages) {
for (Message msg in messages) { for (final msg in messages) {
// if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) { // if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) {
// allMediaFiles.add(msg); // allMediaFiles.add(msg);
// } // }
// Find the index of the existing message with the same messageId // Find the index of the existing message with the same messageId
int index = final index =
allMediaFiles.indexWhere((m) => m.messageId == msg.messageId); allMediaFiles.indexWhere((m) => m.messageId == msg.messageId);
if (index >= 1) { if (index >= 1) {
@ -114,24 +117,23 @@ class _MediaViewerViewState extends State<MediaViewerView> {
setState(() {}); setState(() {});
if (firstRun) { if (firstRun) {
loadCurrentMediaFile(); loadCurrentMediaFile();
firstRun = false;
} }
}); });
} }
Future nextMediaOrExit() async { Future<void> nextMediaOrExit() async {
if (!mounted) return; if (!mounted) return;
videoController?.dispose(); await videoController?.dispose();
nextMediaTimer?.cancel(); nextMediaTimer?.cancel();
progressTimer?.cancel(); progressTimer?.cancel();
if (allMediaFiles.isNotEmpty) { if (allMediaFiles.isNotEmpty) {
try { try {
if (!imageSaved && maxShowTime != gMediaShowInfinite) { if (!imageSaved && maxShowTime != gMediaShowInfinite) {
await deleteMediaFile(allMediaFiles.first.messageId, "mp4"); await deleteMediaFile(allMediaFiles.first.messageId, 'mp4');
await deleteMediaFile(allMediaFiles.first.messageId, "png"); await deleteMediaFile(allMediaFiles.first.messageId, 'png');
} }
} catch (e) { } catch (e) {
Log.error("$e"); Log.error('$e');
} }
} }
if (allMediaFiles.isEmpty || allMediaFiles.length == 1) { if (allMediaFiles.isEmpty || allMediaFiles.length == 1) {
@ -144,7 +146,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
} }
Future loadCurrentMediaFile({bool showTwonly = false}) async { Future<void> loadCurrentMediaFile({bool showTwonly = false}) async {
if (!mounted) return; if (!mounted) return;
if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit(); if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit();
await _noScreenshot.screenshotOff(); await _noScreenshot.screenshotOff();
@ -165,9 +167,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}); });
if (Platform.isAndroid) { if (Platform.isAndroid) {
flutterLocalNotificationsPlugin.cancel(allMediaFiles.first.contactId); await flutterLocalNotificationsPlugin
.cancel(allMediaFiles.first.contactId);
} else { } else {
flutterLocalNotificationsPlugin.cancelAll(); await flutterLocalNotificationsPlugin.cancelAll();
} }
if (allMediaFiles.first.downloadState != DownloadState.downloaded) { if (allMediaFiles.first.downloadState != DownloadState.downloaded) {
@ -179,14 +182,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
final stream = twonlyDB.messagesDao final stream = twonlyDB.messagesDao
.getMessageByMessageId(allMediaFiles.first.messageId) .getMessageByMessageId(allMediaFiles.first.messageId)
.watchSingleOrNull(); .watchSingleOrNull();
downloadStateListener?.cancel(); await downloadStateListener?.cancel();
downloadStateListener = stream.listen((updated) async { downloadStateListener = stream.listen((updated) async {
if (updated != null) { if (updated != null) {
if (updated.downloadState == DownloadState.downloaded) { if (updated.downloadState == DownloadState.downloaded) {
downloadStateListener?.cancel(); await downloadStateListener?.cancel();
await handleNextDownloadedMedia(updated, showTwonly); await handleNextDownloadedMedia(updated, showTwonly);
// start downloading all the other possible missing media files. // start downloading all the other possible missing media files.
tryDownloadAllMediaFiles(force: true); await tryDownloadAllMediaFiles(force: true);
} }
} }
}); });
@ -195,9 +198,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
} }
Future handleNextDownloadedMedia(Message current, bool showTwonly) async { Future<void> handleNextDownloadedMedia(
final MediaMessageContent content = Message current, bool showTwonly) async {
MediaMessageContent.fromJson(jsonDecode(current.contentJson!)); final content =
MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map);
if (content.isRealTwonly) { if (content.isRealTwonly) {
setState(() { setState(() {
@ -205,12 +209,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}); });
if (!showTwonly) return; if (!showTwonly) return;
bool isAuth = await authenticateUser( final isAuth = await authenticateUser(
context.lang.mediaViewerAuthReason, context.lang.mediaViewerAuthReason,
force: false, force: false,
); );
if (!isAuth) { if (!isAuth) {
nextMediaOrExit(); await nextMediaOrExit();
return; return;
} }
} }
@ -229,8 +233,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
final videoPathTmp = await getVideoPath(current.messageId); final videoPathTmp = await getVideoPath(current.messageId);
if (videoPathTmp != null) { if (videoPathTmp != null) {
videoController = VideoPlayerController.file(File(videoPathTmp.path)); videoController = VideoPlayerController.file(File(videoPathTmp.path));
videoController?.setLooping(content.maxShowTime == gMediaShowInfinite); await videoController
videoController?.initialize().then((_) { ?.setLooping(content.maxShowTime == gMediaShowInfinite);
await videoController?.initialize().then((_) {
videoController!.play(); videoController!.play();
videoController?.addListener(() { videoController?.addListener(() {
setState(() { setState(() {
@ -248,9 +253,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
setState(() { setState(() {
videoPath = videoPathTmp.path; videoPath = videoPathTmp.path;
}); });
}).catchError((Object error) { // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
Log.error(error); }).catchError(Log.error);
});
} }
} }
@ -258,7 +262,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if ((imageBytes == null && !content.isVideo) || if ((imageBytes == null && !content.isVideo) ||
(content.isVideo && videoController == null)) { (content.isVideo && videoController == null)) {
Log.error("media files are not found..."); Log.error('media files are not found...');
// When the message should be downloaded but imageBytes are null then a error happened // When the message should be downloaded but imageBytes are null then a error happened
await handleMediaError(current); await handleMediaError(current);
return nextMediaOrExit(); return nextMediaOrExit();
@ -287,17 +291,17 @@ class _MediaViewerViewState extends State<MediaViewerView> {
nextMediaOrExit(); nextMediaOrExit();
} }
}); });
progressTimer = Timer.periodic(Duration(milliseconds: 10), (timer) { progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
if (canBeSeenUntil != null) { if (canBeSeenUntil != null) {
Duration difference = canBeSeenUntil!.difference(DateTime.now()); final difference = canBeSeenUntil!.difference(DateTime.now());
// Calculate the progress as a value between 0.0 and 1.0 // Calculate the progress as a value between 0.0 and 1.0
progress = (difference.inMilliseconds / (maxShowTime * 1000)); progress = difference.inMilliseconds / (maxShowTime * 1000);
setState(() {}); setState(() {});
} }
}); });
} }
Future onPressedSaveToGallery() async { Future<void> onPressedSaveToGallery() async {
if (allMediaFiles.first.messageOtherId == null) { if (allMediaFiles.first.messageOtherId == null) {
return; // should not be possible return; // should not be possible
} }
@ -306,7 +310,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}); });
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
allMediaFiles.first.messageId, allMediaFiles.first.messageId,
MessagesCompanion(mediaStored: Value(true)), const MessagesCompanion(mediaStored: Value(true)),
); );
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
@ -314,7 +318,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
MessageJson( MessageJson(
kind: MessageKind.storedMediaFile, kind: MessageKind.storedMediaFile,
messageSenderId: allMediaFiles.first.messageId, messageSenderId: allMediaFiles.first.messageId,
messageReceiverId: allMediaFiles.first.messageOtherId!, messageReceiverId: allMediaFiles.first.messageOtherId,
content: MessageContent(), content: MessageContent(),
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
@ -337,8 +341,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
void displayShortReactions() { void displayShortReactions() {
RenderBox renderBox = final renderBox =
mediaWidgetKey.currentContext?.findRenderObject() as RenderBox; mediaWidgetKey.currentContext!.findRenderObject()! as RenderBox;
setState(() { setState(() {
showShortReactions = true; showShortReactions = true;
mediaViewerDistanceFromBottom = renderBox.size.height; mediaViewerDistanceFromBottom = renderBox.size.height;
@ -349,7 +353,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
return Row( return Row(
key: mediaWidgetKey, key: mediaWidgetKey,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (maxShowTime == gMediaShowInfinite) if (maxShowTime == gMediaShowInfinite)
OutlinedButton( OutlinedButton(
@ -364,18 +367,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
onPressed: onPressedSaveToGallery, onPressed: onPressedSaveToGallery,
child: Row( child: Row(
children: [ children: [
imageSaving if (imageSaving)
? SizedBox( const SizedBox(
width: 10, width: 10,
height: 10, height: 10,
child: CircularProgressIndicator(strokeWidth: 1)) child: CircularProgressIndicator(strokeWidth: 1))
: imageSaved else
? Icon(Icons.check) imageSaved
: FaIcon(FontAwesomeIcons.floppyDisk), ? const Icon(Icons.check)
: const FaIcon(FontAwesomeIcons.floppyDisk),
], ],
), ),
), ),
SizedBox(width: 10), const SizedBox(width: 10),
IconButton( IconButton(
icon: SizedBox( icon: SizedBox(
width: 30, width: 30,
@ -410,13 +414,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20), const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
), ),
), ),
), ),
SizedBox(width: 10), const SizedBox(width: 10),
IconButton.outlined( IconButton.outlined(
icon: FaIcon(FontAwesomeIcons.message), icon: const FaIcon(FontAwesomeIcons.message),
onPressed: () async { onPressed: () async {
displayShortReactions(); displayShortReactions();
setState(() { setState(() {
@ -425,31 +429,32 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20), const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
), ),
), ),
), ),
SizedBox(width: 10), const SizedBox(width: 10),
IconButton.outlined( IconButton.outlined(
icon: FaIcon(FontAwesomeIcons.camera), icon: const FaIcon(FontAwesomeIcons.camera),
onPressed: () async { onPressed: () async {
nextMediaTimer?.cancel(); nextMediaTimer?.cancel();
progressTimer?.cancel(); progressTimer?.cancel();
videoController?.pause(); await videoController?.pause();
if (!mounted) return;
await Navigator.push(context, MaterialPageRoute( await Navigator.push(context, MaterialPageRoute(
builder: (context) { builder: (context) {
return CameraSendToView(widget.contact); return CameraSendToView(widget.contact);
}, },
)); ));
if (mounted && maxShowTime != gMediaShowInfinite) { if (mounted && maxShowTime != gMediaShowInfinite) {
nextMediaOrExit(); await nextMediaOrExit();
} else { } else {
videoController?.play(); await videoController?.play();
} }
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20), const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
), ),
), ),
), ),
@ -494,7 +499,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: Image.memory( child: Image.memory(
imageBytes!, imageBytes!,
fit: BoxFit.contain, fit: BoxFit.contain,
frameBuilder: ((context, child, frame, frameBuilder: (context, child, frame,
wasSynchronouslyLoaded) { wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) return child; if (wasSynchronouslyLoaded) return child;
return AnimatedSwitcher( return AnimatedSwitcher(
@ -505,12 +510,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
height: 60, height: 60,
color: Colors.transparent, color: Colors.transparent,
width: 60, width: 60,
child: CircularProgressIndicator( child: const CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
), ),
), ),
); );
}), },
), ),
), ),
], ],
@ -531,7 +536,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
), ),
Container( Container(
padding: EdgeInsets.only(bottom: 200), padding: const EdgeInsets.only(bottom: 200),
child: Text(context.lang.mediaViewerTwonlyTapToOpen), child: Text(context.lang.mediaViewerTwonlyTapToOpen),
), ),
], ],
@ -544,7 +549,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
icon: Icon(Icons.close, size: 30), icon: const Icon(Icons.close, size: 30),
color: Colors.white, color: Colors.white,
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
@ -554,7 +559,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
), ),
if (isDownloading) if (isDownloading)
Positioned.fill( const Positioned.fill(
child: Center( child: Center(
child: SizedBox( child: SizedBox(
height: 60, height: 60,
@ -574,7 +579,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
height: 20, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: progress, value: progress,
strokeWidth: 2.0, strokeWidth: 2,
), ),
), ),
], ],
@ -588,13 +593,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: Text( child: Text(
getContactDisplayName(widget.contact), getContactDisplayName(widget.contact),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
shadows: [ shadows: [
Shadow( Shadow(
color: const Color.fromARGB(122, 0, 0, 0), color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5.0, blurRadius: 5,
) )
], ],
), ),
@ -612,7 +617,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.xmark), icon: const FaIcon(FontAwesomeIcons.xmark),
onPressed: () { onPressed: () {
setState(() { setState(() {
showShortReactions = false; showShortReactions = false;
@ -634,7 +639,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
), ),
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane), icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () { onPressed: () {
if (textMessageController.text.isNotEmpty) { if (textMessageController.text.isNotEmpty) {
sendTextMessage( sendTextMessage(
@ -684,7 +689,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
class ReactionButtons extends StatefulWidget { class ReactionButtons extends StatefulWidget {
const ReactionButtons({ const ReactionButtons({
super.key,
required this.show, required this.show,
required this.textInputFocused, required this.textInputFocused,
required this.userId, required this.userId,
@ -692,6 +696,7 @@ class ReactionButtons extends StatefulWidget {
required this.responseToMessageId, required this.responseToMessageId,
required this.isVideo, required this.isVideo,
required this.hide, required this.hide,
super.key,
}); });
final double mediaViewerDistanceFromBottom; final double mediaViewerDistanceFromBottom;
@ -700,7 +705,7 @@ class ReactionButtons extends StatefulWidget {
final bool textInputFocused; final bool textInputFocused;
final int userId; final int userId;
final int responseToMessageId; final int responseToMessageId;
final Function() hide; final void Function() hide;
@override @override
State<ReactionButtons> createState() => _ReactionButtonsState(); State<ReactionButtons> createState() => _ReactionButtonsState();
@ -718,8 +723,8 @@ class _ReactionButtonsState extends State<ReactionButtons> {
initAsync(); initAsync();
} }
Future initAsync() async { Future<void> initAsync() async {
var user = await getUser(); final user = await getUser();
if (user != null && user.preSelectedEmojies != null) { if (user != null && user.preSelectedEmojies != null) {
selectedEmojis = user.preSelectedEmojies!; selectedEmojis = user.preSelectedEmojies!;
} }
@ -733,7 +738,7 @@ class _ReactionButtonsState extends State<ReactionButtons> {
selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : []; selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : [];
return AnimatedPositioned( return AnimatedPositioned(
duration: Duration(milliseconds: 200), // Animation duration duration: const Duration(milliseconds: 200), // Animation duration
bottom: widget.show bottom: widget.show
? (widget.textInputFocused ? (widget.textInputFocused
? 50 ? 50
@ -744,10 +749,11 @@ class _ReactionButtonsState extends State<ReactionButtons> {
curve: Curves.linearToEaseOut, curve: Curves.linearToEaseOut,
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: widget.show ? 1.0 : 0.0, // Fade in/out opacity: widget.show ? 1.0 : 0.0, // Fade in/out
duration: Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
child: Container( child: Container(
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
padding: widget.show ? EdgeInsets.symmetric(vertical: 32) : null, padding:
widget.show ? const EdgeInsets.symmetric(vertical: 32) : null,
child: Column( child: Column(
children: [ children: [
if (secondRowEmojis.isNotEmpty) if (secondRowEmojis.isNotEmpty)
@ -761,11 +767,11 @@ class _ReactionButtonsState extends State<ReactionButtons> {
hide: widget.hide, hide: widget.hide,
show: widget.show, show: widget.show,
isVideo: widget.isVideo, isVideo: widget.isVideo,
emoji: emoji, emoji: emoji as String,
)) ))
.toList(), .toList(),
), ),
if (secondRowEmojis.isNotEmpty) SizedBox(height: 15), if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
@ -791,22 +797,21 @@ class _ReactionButtonsState extends State<ReactionButtons> {
} }
class EmojiReactionWidget extends StatefulWidget { class EmojiReactionWidget extends StatefulWidget {
final int userId;
final int responseToMessageId;
final Function hide;
final bool show;
final bool isVideo;
final String emoji;
const EmojiReactionWidget({ const EmojiReactionWidget({
super.key,
required this.userId, required this.userId,
required this.responseToMessageId, required this.responseToMessageId,
required this.hide, required this.hide,
required this.isVideo, required this.isVideo,
required this.show, required this.show,
required this.emoji, required this.emoji,
super.key,
}); });
final int userId;
final int responseToMessageId;
final Function hide;
final bool show;
final bool isVideo;
final String emoji;
@override @override
State<EmojiReactionWidget> createState() => _EmojiReactionWidgetState(); State<EmojiReactionWidget> createState() => _EmojiReactionWidgetState();
@ -818,7 +823,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedSize( return AnimatedSize(
duration: Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
curve: Curves.linearToEaseOut, curve: Curves.linearToEaseOut,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
@ -838,7 +843,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
setState(() { setState(() {
selectedShortReaction = 0; // Assuming index is 0 for this example selectedShortReaction = 0; // Assuming index is 0 for this example
}); });
Future.delayed(Duration(milliseconds: 300), () { Future.delayed(const Duration(milliseconds: 300), () {
setState(() { setState(() {
widget.hide(); widget.hide();
selectedShortReaction = -1; selectedShortReaction = -1;
@ -849,13 +854,13 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
0) // Assuming index is 0 for this example 0) // Assuming index is 0 for this example
? EmojiAnimationFlying( ? EmojiAnimationFlying(
emoji: widget.emoji, emoji: widget.emoji,
duration: Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
startPosition: 0.0, startPosition: 0,
size: (widget.show) ? 40 : 10, size: (widget.show) ? 40 : 10,
) )
: AnimatedOpacity( : AnimatedOpacity(
opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out
duration: Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
child: SizedBox( child: SizedBox(
width: widget.show ? 40 : 10, width: widget.show ? 40 : 10,
child: Center( child: Center(

View file

@ -1,17 +1,18 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/user_context_menu.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/user_context_menu.dart';
class StartNewChatView extends StatefulWidget { class StartNewChatView extends StatefulWidget {
const StartNewChatView({super.key}); const StartNewChatView({super.key});
@ -29,8 +30,7 @@ class _StartNewChatView extends State<StartNewChatView> {
void initState() { void initState() {
super.initState(); super.initState();
Stream<List<Contact>> stream = final stream = twonlyDB.contactsDao.watchContactsForStartNewChat();
twonlyDB.contactsDao.watchContactsForStartNewChat();
contactSub = stream.listen((update) { contactSub = stream.listen((update) {
update.sort((a, b) => update.sort((a, b) =>
@ -48,14 +48,14 @@ class _StartNewChatView extends State<StartNewChatView> {
contactSub.cancel(); contactSub.cancel();
} }
Future filterUsers() async { Future<void> filterUsers() async {
if (searchUserName.value.text.isEmpty) { if (searchUserName.value.text.isEmpty) {
setState(() { setState(() {
contacts = allContacts; contacts = allContacts;
}); });
return; return;
} }
List<Contact> usersFiltered = allContacts final usersFiltered = allContacts
.where((user) => getContactDisplayName(user) .where((user) => getContactDisplayName(user)
.toLowerCase() .toLowerCase()
.contains(searchUserName.value.text.toLowerCase())) .contains(searchUserName.value.text.toLowerCase()))
@ -75,11 +75,12 @@ class _StartNewChatView extends State<StartNewChatView> {
child: PieCanvas( child: PieCanvas(
theme: getPieCanvasTheme(context), theme: getPieCanvasTheme(context),
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), padding:
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField( child: TextField(
onChanged: (_) { onChanged: (_) {
filterUsers(); filterUsers();
@ -121,9 +122,9 @@ class UserList extends StatelessWidget {
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
if (i == 0) { if (i == 0) {
return ListTile( return ListTile(
key: Key("add_new_contact"), key: const Key('add_new_contact'),
title: Text(context.lang.startNewChatNewContact), title: Text(context.lang.startNewChatNewContact),
leading: CircleAvatar( leading: const CircleAvatar(
child: FaIcon( child: FaIcon(
FontAwesomeIcons.userPlus, FontAwesomeIcons.userPlus,
size: 13, size: 13,
@ -133,24 +134,22 @@ class UserList extends StatelessWidget {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => AddNewUserView(), builder: (context) => const AddNewUserView(),
), ),
); );
}, },
); );
} }
if (i == 1) { if (i == 1) {
return Divider(); return const Divider();
} }
Contact user = users[i - 2]; final user = users[i - 2];
int flameCounter = getFlameCounterFromContact(user); final flameCounter = getFlameCounterFromContact(user);
return UserContextMenu( return UserContextMenu(
key: Key(user.userId.toString()), key: Key(user.userId.toString()),
contact: user, contact: user,
child: ListTile( child: ListTile(
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text(getContactDisplayName(user)), Text(getContactDisplayName(user)),
if (flameCounter >= 1) if (flameCounter >= 1)
@ -159,14 +158,14 @@ class UserList extends StatelessWidget {
flameCounter, flameCounter,
prefix: true, prefix: true,
), ),
Spacer(), const Spacer(),
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.boxOpen, icon: FaIcon(FontAwesomeIcons.boxOpen,
size: 13, size: 13,
color: user.archived ? null : Colors.transparent), color: user.archived ? null : Colors.transparent),
onPressed: user.archived onPressed: user.archived
? () async { ? () async {
final update = const update =
ContactsCompanion(archived: Value(false)); ContactsCompanion(archived: Value(false));
await twonlyDB.contactsDao await twonlyDB.contactsDao
.updateContact(user.userId, update); .updateContact(user.userId, update);

View file

@ -10,9 +10,9 @@ Future<bool> showAlertDialog(
String? customOk, String? customOk,
String? customCancel, String? customCancel,
}) async { }) async {
Completer<bool> completer = Completer<bool>(); final completer = Completer<bool>();
Widget okButton = TextButton( final Widget okButton = TextButton(
child: Text(customOk ?? context.lang.ok), child: Text(customOk ?? context.lang.ok),
onPressed: () { onPressed: () {
completer.complete(true); completer.complete(true);
@ -20,7 +20,7 @@ Future<bool> showAlertDialog(
}, },
); );
Widget cancelButton = TextButton( final Widget cancelButton = TextButton(
child: Text(customCancel ?? context.lang.cancel), child: Text(customCancel ?? context.lang.cancel),
onPressed: () { onPressed: () {
completer.complete(false); completer.complete(false);
@ -29,7 +29,7 @@ Future<bool> showAlertDialog(
); );
// set up the AlertDialog // set up the AlertDialog
AlertDialog alert = AlertDialog( final alert = AlertDialog(
title: Text(title), title: Text(title),
content: (content == null) ? null : Text(content), content: (content == null) ? null : Text(content),
actions: [ actions: [
@ -39,7 +39,8 @@ Future<bool> showAlertDialog(
); );
// show the dialog // show the dialog
showDialog( // ignore: inference_failure_on_function_invocation
await showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return alert; return alert;

View file

@ -17,171 +17,170 @@ bool isEmoji(String character) {
} }
class EmojiAnimation extends StatelessWidget { class EmojiAnimation extends StatelessWidget {
const EmojiAnimation({required this.emoji, super.key, this.repeat = true});
final String emoji; final String emoji;
final bool repeat; final bool repeat;
static final Map<String, String> animatedIcons = { static final Map<String, String> animatedIcons = {
"": "red_heart.json", '': 'red_heart.json',
"😂": "joy.json", '😂': 'joy.json',
"🔥": "fire.json", '🔥': 'fire.json',
"💪": "muscle.json", '💪': 'muscle.json',
"😭": "loudly-crying.json", '😭': 'loudly-crying.json',
"🤯": "mind-blown.json", '🤯': 'mind-blown.json',
"❤️‍🔥": "red_heart_fire.json", '❤️‍🔥': 'red_heart_fire.json',
"😁": "grinning.json", '😁': 'grinning.json',
"😆": "laughing.json", '😆': 'laughing.json',
"😅": "grin-sweat.json", '😅': 'grin-sweat.json',
"🤣": "rofl.json", '🤣': 'rofl.json',
"😉": "wink.json", '😉': 'wink.json',
"😘": "kissing-heart.json", '😘': 'kissing-heart.json',
"🥰": "heart-face.json", '🥰': 'heart-face.json',
"😍": "heart-eyes.json", '😍': 'heart-eyes.json',
"🤩": "star-struck.json", '🤩': 'star-struck.json',
"🥳": "partying-face.json", '🥳': 'partying-face.json',
"🙃": "upside-down-face.json", '🙃': 'upside-down-face.json',
"🥲": "happy-cry.json", '🥲': 'happy-cry.json',
"😊": "blush.json", '😊': 'blush.json',
"😏": "smirk.json", '😏': 'smirk.json',
"🤤": "drool.json", '🤤': 'drool.json',
"😋": "yum.json", '😋': 'yum.json',
"😛": "stuck-out-tongue.json", '😛': 'stuck-out-tongue.json',
"🤪": "zany-face.json", '🤪': 'zany-face.json',
"🥴": "woozy.json", '🥴': 'woozy.json',
"😔": "pensive.json", '😔': 'pensive.json',
"🥺": "pleading.json", '🥺': 'pleading.json',
"😬": "grimacing.json", '😬': 'grimacing.json',
"😑": "expressionless.json", '😑': 'expressionless.json',
"🤐": "zipper-face.json", '🤐': 'zipper-face.json',
"🤔": "thinking-face.json", '🤔': 'thinking-face.json',
"🥱": "yawn.json", '🥱': 'yawn.json',
"🤗": "hug-face.json", '🤗': 'hug-face.json',
"😱": "screaming.json", '😱': 'screaming.json',
"🤨": "raised-eyebrow.json", '🤨': 'raised-eyebrow.json',
"🧐": "monocle.json", '🧐': 'monocle.json',
"😒": "unamused.json", '😒': 'unamused.json',
"🙄": "rolling-eyes.json", '🙄': 'rolling-eyes.json',
"😤": "triumph.json", '😤': 'triumph.json',
"🤬": "cursing.json", '🤬': 'cursing.json',
"😞": "sad.json", '😞': 'sad.json',
"😢": "cry.json", '😢': 'cry.json',
"🙁": "frown.json", '🙁': 'frown.json',
"😨": "scared.json", '😨': 'scared.json',
"😳": "flushed.json", '😳': 'flushed.json',
"😖": "scrunched-mouth.json", '😖': 'scrunched-mouth.json',
"😵": "x-eyes.json", '😵': 'x-eyes.json',
"🥶": "cold-face.json", '🥶': 'cold-face.json',
"🥵": "hot-face.json", '🥵': 'hot-face.json',
"🤮": "vomit.json", '🤮': 'vomit.json',
"😴": "sleep.json", '😴': 'sleep.json',
"🤒": "thermometer-face.json", '🤒': 'thermometer-face.json',
"🤕": "bandage-face.json", '🤕': 'bandage-face.json',
"🤥": "liar.json", '🤥': 'liar.json',
"😇": "halo.json", '😇': 'halo.json',
"🤠": "cowboy.json", '🤠': 'cowboy.json',
"🤑": "money-face.json", '🤑': 'money-face.json',
"🤓": "nerd-face.json", '🤓': 'nerd-face.json',
"😎": "sunglasses-face.json", '😎': 'sunglasses-face.json',
"🥸": "disguise.json", '🥸': 'disguise.json',
"🤡": "clown.json", '🤡': 'clown.json',
"💩": "poop.json", '💩': 'poop.json',
"😈": "imp-smile.json", '😈': 'imp-smile.json',
"👻": "ghost.json", '👻': 'ghost.json',
"💀": "skull.json", '💀': 'skull.json',
"": "snowman.json", '': 'snowman.json',
"🎃": "jack-o-lantern.json", '🎃': 'jack-o-lantern.json',
"🤖": "robot.json", '🤖': 'robot.json',
"👽": "alien.json", '👽': 'alien.json',
"🙈": "see-no-evil-monkey.json", '🙈': 'see-no-evil-monkey.json',
"🙉": "hear-no-evil-monkey.json", '🙉': 'hear-no-evil-monkey.json',
"🙊": "speak-no-evil-monkey.json", '🙊': 'speak-no-evil-monkey.json',
"🌟": "glowing-star.json", '🌟': 'glowing-star.json',
"": "sparkles.json", '': 'sparkles.json',
"": "electricity.json", '': 'electricity.json',
"💥": "collision.json", '💥': 'collision.json',
"💯": "100.json", '💯': '100.json',
"🎉": "party-popper.json", '🎉': 'party-popper.json',
"🎊": "confetti-ball.json", '🎊': 'confetti-ball.json',
"🧡": "orange-heart.json", '🧡': 'orange-heart.json',
"💛": "yellow-heart.json", '💛': 'yellow-heart.json',
"💚": "green-heart.json", '💚': 'green-heart.json',
"💙": "blue-heart.json", '💙': 'blue-heart.json',
"💜": "purple-heart.json", '💜': 'purple-heart.json',
"💘": "cupid.json", '💘': 'cupid.json',
"💝": "gift-heart.json", '💝': 'gift-heart.json',
"💖": "sparkling-heart.json", '💖': 'sparkling-heart.json',
"💕": "two-hearts.json", '💕': 'two-hearts.json',
"💔": "broken-heart.json", '💔': 'broken-heart.json',
"💋": "kiss.json", '💋': 'kiss.json',
"👀": "eyes.json", '👀': 'eyes.json',
"🦻": "hearing-aid.json", '🦻': 'hearing-aid.json',
"🦶": "foot.json", '🦶': 'foot.json',
"🦾": "arm-mechanical.json", '🦾': 'arm-mechanical.json',
"👏": "clap.json", '👏': 'clap.json',
"👍": "thumbs-up.json", '👍': 'thumbs-up.json',
"👎": "thumbs-down.json", '👎': 'thumbs-down.json',
"🙌": "raising-hands.json", '🙌': 'raising-hands.json',
"": "raised-fist.json", '': 'raised-fist.json',
"👊": "fist.json", '👊': 'fist.json',
"👋": "wave.json", '👋': 'wave.json',
"🤘": "metal.json", '🤘': 'metal.json',
"🤞": "crossed-fingers.json", '🤞': 'crossed-fingers.json',
"🤙": "call-me-hand.json", '🤙': 'call-me-hand.json',
"👌": "ok.json", '👌': 'ok.json',
"🖕": "middle-finger.json", '🖕': 'middle-finger.json',
"🤝": "handshake.json", '🤝': 'handshake.json',
"💃": "dancer-woman.json", '💃': 'dancer-woman.json',
"🌱": "plant.json", '🌱': 'plant.json',
"🍃": "leaves.json", '🍃': 'leaves.json',
"🍀": "luck.json", '🍀': 'luck.json',
"🌊": "ocean.json", '🌊': 'ocean.json',
"💧": "droplet.json", '💧': 'droplet.json',
"🦄": "unicorn.json", '🦄': 'unicorn.json',
"🦖": "t-rex.json", '🦖': 't-rex.json',
"🦕": "dinosaur.json", '🦕': 'dinosaur.json',
"🐢": "turtle.json", '🐢': 'turtle.json',
"🐍": "snake.json", '🐍': 'snake.json',
"🐩": "poodle.json", '🐩': 'poodle.json',
"🐕": "dog.json", '🐕': 'dog.json',
"🐖": "pig.json", '🐖': 'pig.json',
"🦘": "kangaroo.json", '🦘': 'kangaroo.json',
"🦍": "gorilla.json", '🦍': 'gorilla.json',
"🦧": "orangutan.json", '🦧': 'orangutan.json',
"🦦": "otter.json", '🦦': 'otter.json',
"🐓": "rooster.json", '🐓': 'rooster.json',
"🦅": "eagle.json", '🦅': 'eagle.json',
"🦉": "owl.json", '🦉': 'owl.json',
"🐬": "dolphin.json", '🐬': 'dolphin.json',
"🐳": "whale.json", '🐳': 'whale.json',
"🐟": "fish.json", '🐟': 'fish.json',
"🐡": "blowfish.json", '🐡': 'blowfish.json',
"🦀": "crab.json", '🦀': 'crab.json',
"🐙": "octopus.json", '🐙': 'octopus.json',
"🐌": "snail.json", '🐌': 'snail.json',
"🍻": "clinking-beer-mugs.json", '🍻': 'clinking-beer-mugs.json',
"🍾": "bottle-with-popping-cork.json", '🍾': 'bottle-with-popping-cork.json',
"🚨": "police-car-light.json", '🚨': 'police-car-light.json',
"🛸": "flying-saucer.json", '🛸': 'flying-saucer.json',
"🚀": "rocket.json", '🚀': 'rocket.json',
"🛫": "airplane-departure.json", '🛫': 'airplane-departure.json',
"🎢": "roller-coaster.json", '🎢': 'roller-coaster.json',
"🎡": "ferris-wheel.json", '🎡': 'ferris-wheel.json',
"🎈": "balloon.json", '🎈': 'balloon.json',
"🎁": "wrapped-gift.json", '🎁': 'wrapped-gift.json',
"🎆": "fireworks.json", '🎆': 'fireworks.json',
"💸": "money-with-wings.json", '💸': 'money-with-wings.json',
"💎": "gem-stone.json", '💎': 'gem-stone.json',
"🎓": "graduation-cap.json", '🎓': 'graduation-cap.json',
"🔔": "bell.json", '🔔': 'bell.json',
"💣": "bomb.json", '💣': 'bomb.json',
"": "exclamation.json", '': 'exclamation.json',
"": "question.json", '': 'question.json',
"": "cross-mark.json", '': 'cross-mark.json',
"🏁": "chequered-flag.json", '🏁': 'chequered-flag.json',
"🚩": "triangular-flag.json", '🚩': 'triangular-flag.json',
"🏴": "black-flag.json", '🏴': 'black-flag.json',
}; };
const EmojiAnimation({super.key, required this.emoji, this.repeat = true});
static bool supported(String emoji) { static bool supported(String emoji) {
if (emoji.length > 4) return false; if (emoji.length > 4) return false;
return animatedIcons.containsKey(emoji) || isEmoji(emoji); return animatedIcons.containsKey(emoji) || isEmoji(emoji);
@ -194,39 +193,38 @@ class EmojiAnimation extends StatelessWidget {
// Check if the emoji has a corresponding Lottie animation // Check if the emoji has a corresponding Lottie animation
if (animatedIcons.containsKey(emoji)) { if (animatedIcons.containsKey(emoji)) {
return Lottie.asset( return Lottie.asset(
"assets/animated_icons/${animatedIcons[emoji]}", 'assets/animated_icons/${animatedIcons[emoji]}',
repeat: repeat, repeat: repeat,
); );
} else if (isEmoji(emoji)) { } else if (isEmoji(emoji)) {
return Text( return Text(
emoji, emoji,
style: TextStyle(fontSize: 60), style: const TextStyle(fontSize: 60),
); );
} else { } else {
return Text( return Text(
emoji, emoji,
style: TextStyle(fontSize: 15), style: const TextStyle(fontSize: 15),
); );
} }
} }
} }
class EmojiAnimationFlying extends StatelessWidget { class EmojiAnimationFlying extends StatelessWidget {
const EmojiAnimationFlying({
required this.emoji,
required this.duration,
required this.startPosition,
required this.size,
super.key,
this.repeat = true,
});
final String emoji; final String emoji;
final Duration duration; final Duration duration;
final double startPosition; final double startPosition;
final int size; final int size;
final bool repeat; final bool repeat;
const EmojiAnimationFlying({
super.key,
required this.emoji,
required this.duration,
required this.startPosition,
required this.size,
this.repeat = true,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TweenAnimationBuilder<double>( return TweenAnimationBuilder<double>(

View file

@ -0,0 +1,86 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:url_launcher/url_launcher.dart';
class AppOutdated extends StatefulWidget {
const AppOutdated({super.key});
@override
State<AppOutdated> createState() => _AppOutdatedState();
}
class _AppOutdatedState extends State<AppOutdated> {
bool appIsOutdated = false;
@override
void dispose() {
globalCallbackAppIsOutdated = () {};
super.dispose();
}
Future<void> initAsync() async {
globalCallbackAppIsOutdated = () async {
await context.read<CustomChangeProvider>().updateConnectionState(false);
setState(() {
appIsOutdated = true;
});
};
}
@override
Widget build(BuildContext context) {
if (!appIsOutdated) return Container();
return Positioned(
top: 60,
left: 30,
right: 30,
child: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
context.lang.appOutdated,
textAlign: TextAlign.center,
softWrap: true,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
if (Platform.isAndroid) const SizedBox(height: 5),
if (Platform.isAndroid)
ElevatedButton(
onPressed: () {
launchUrl(Uri.parse(
'https://play.google.com/store/apps/details?id=eu.twonly'));
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
context.lang.appOutdatedBtn,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
),
],
),
),
),
);
}
}

View file

@ -2,6 +2,14 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class BetterListTile extends StatelessWidget { class BetterListTile extends StatelessWidget {
const BetterListTile(
{required this.icon,
required this.text,
required this.onTap,
super.key,
this.color,
this.subtitle,
this.iconSize = 20});
final IconData icon; final IconData icon;
final String text; final String text;
final Widget? subtitle; final Widget? subtitle;
@ -9,15 +17,6 @@ class BetterListTile extends StatelessWidget {
final VoidCallback onTap; final VoidCallback onTap;
final double iconSize; final double iconSize;
const BetterListTile(
{super.key,
required this.icon,
required this.text,
this.color,
this.subtitle,
required this.onTap,
this.iconSize = 20});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(

View file

@ -1,36 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class BetterText extends StatelessWidget { class BetterText extends StatelessWidget {
const BetterText({required this.text, super.key});
final String text; final String text;
const BetterText({super.key, required this.text});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Regular expression to find URLs and domains // Regular expression to find URLs and domains
final RegExp urlRegExp = RegExp( final urlRegExp = RegExp(
r'(?:(?:https?://|www\.)[^\s]+|(?:[a-zA-Z0-9-]+\.[a-zA-Z]{2,}))', r'(?:(?:https?://|www\.)[^\s]+|(?:[a-zA-Z0-9-]+\.[a-zA-Z]{2,}))',
caseSensitive: false, caseSensitive: false,
multiLine: false,
); );
final List<TextSpan> spans = []; final spans = <TextSpan>[];
final Iterable<RegExpMatch> matches = urlRegExp.allMatches(text); final matches = urlRegExp.allMatches(text);
int lastMatchEnd = 0; var lastMatchEnd = 0;
for (final match in matches) { for (final match in matches) {
if (match.start > lastMatchEnd) { if (match.start > lastMatchEnd) {
spans.add(TextSpan(text: text.substring(lastMatchEnd, match.start))); spans.add(TextSpan(text: text.substring(lastMatchEnd, match.start)));
} }
final String? url = match.group(0); final url = match.group(0);
spans.add(TextSpan( spans.add(TextSpan(
text: url, text: url,
style: TextStyle(color: Colors.blue), style: const TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () async { ..onTap = () async {
final lUrl = final lUrl =
@ -38,7 +36,7 @@ class BetterText extends StatelessWidget {
try { try {
await launchUrl(lUrl); await launchUrl(lUrl);
} catch (e) { } catch (e) {
Log.error("Could not launch $e"); Log.error('Could not launch $e');
} }
}, },
)); ));
@ -54,7 +52,7 @@ class BetterText extends StatelessWidget {
TextSpan( TextSpan(
children: spans, children: spans,
), ),
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 17, fontSize: 17,
), ),

View file

@ -1,16 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FormattedStringWidget extends StatelessWidget { class FingerprintText extends StatelessWidget {
const FingerprintText(this.longString, {super.key});
final String longString; final String longString;
const FormattedStringWidget(this.longString, {super.key});
String formatString(String input) { String formatString(String input) {
StringBuffer formattedString = StringBuffer(); final formattedString = StringBuffer();
int blockCount = 0; var blockCount = 0;
for (int i = 0; i < input.length; i += 4) { for (var i = 0; i < input.length; i += 4) {
String block = final block =
input.substring(i, i + 4 > input.length ? input.length : i + 4); input.substring(i, i + 4 > input.length ? input.length : i + 4);
formattedString.write(block); formattedString.write(block);
blockCount++; blockCount++;
@ -30,7 +29,7 @@ class FormattedStringWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SelectableText( return SelectableText(
formatString(longString), formatString(longString),
style: TextStyle(fontSize: 16, color: Colors.black), style: const TextStyle(fontSize: 16, color: Colors.black),
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
} }

View file

@ -1,26 +1,25 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
class FlameCounterWidget extends StatelessWidget { class FlameCounterWidget extends StatelessWidget {
final Contact user;
final int flameCounter;
final bool prefix;
const FlameCounterWidget( const FlameCounterWidget(
this.user, this.user,
this.flameCounter, { this.flameCounter, {
this.prefix = false, this.prefix = false,
super.key, super.key,
}); });
final Contact user;
final int flameCounter;
final bool prefix;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
if (prefix) const SizedBox(width: 5), if (prefix) const SizedBox(width: 5),
if (prefix) Text(""), if (prefix) const Text(''),
if (prefix) const SizedBox(width: 5), if (prefix) const SizedBox(width: 5),
Text( Text(
flameCounter.toString(), flameCounter.toString(),
@ -29,7 +28,7 @@ class FlameCounterWidget extends StatelessWidget {
SizedBox( SizedBox(
height: 15, height: 15,
child: EmojiAnimation( child: EmojiAnimation(
emoji: (globalBestFriendUserId == user.userId) ? "❤️‍🔥" : "🔥"), emoji: (globalBestFriendUserId == user.userId) ? '❤️‍🔥' : '🔥'),
), ),
], ],
); );

View file

@ -1,18 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class HeadLineComponent extends StatelessWidget { class HeadLineComponent extends StatelessWidget {
final String text;
const HeadLineComponent(this.text, {super.key}); const HeadLineComponent(this.text, {super.key});
final String text;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 10),
child: Text( child: Text(
text, text,
style: TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
), ),
); );
} }

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