mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 08:18:41 +00:00
enabling linter
This commit is contained in:
parent
0eda03537c
commit
a3d9bcf98b
148 changed files with 4643 additions and 4406 deletions
271
.vscode/launch.json
vendored
271
.vscode/launch.json
vendored
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"files.exclude": {
|
||||
"dependencies": false
|
||||
}
|
||||
}
|
||||
11
README.md
11
README.md
|
|
@ -13,12 +13,15 @@ This repository contains the complete source code of the [twonly](https://twonly
|
|||
|
||||
## In work
|
||||
|
||||
- We plan to implement a Sealed Sender feature to minimize metadata
|
||||
- We currently evaluating to switch from the Signal Protocol to [MLS](https://openmls.tech/).
|
||||
|
||||
- For Android: Using [UnifiedPush](https://unifiedpush.org/) instead of FCM
|
||||
- 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
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# 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`.
|
||||
include: package:very_good_analysis/analysis_options.yaml
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
errors:
|
||||
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:
|
||||
# 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:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
public_member_api_docs: false
|
||||
avoid_catches_without_on_clauses: false
|
||||
document_ignores: false
|
||||
|
|
|
|||
|
|
@ -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
137
lib/app.dart
137
lib/app.dart
|
|
@ -1,24 +1,18 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.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/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/onboarding/onboarding.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 {
|
||||
const App({super.key});
|
||||
@override
|
||||
|
|
@ -27,7 +21,6 @@ class App extends StatefulWidget {
|
|||
|
||||
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
bool wasPaused = false;
|
||||
bool appIsOutdated = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -35,16 +28,15 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
globalIsAppInBackground = false;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// register global callbacks to the widget tree
|
||||
globalCallbackConnectionState = (update) {
|
||||
context.read<CustomChangeProvider>().updateConnectionState(update);
|
||||
globalCallbackConnectionState = ({required bool isConnected}) {
|
||||
context.read<CustomChangeProvider>().updateConnectionState(isConnected);
|
||||
setUserPlan();
|
||||
};
|
||||
|
||||
initAsync();
|
||||
}
|
||||
|
||||
Future setUserPlan() async {
|
||||
Future<void> setUserPlan() async {
|
||||
final user = await getUser();
|
||||
globalBestFriendUserId = -1;
|
||||
if (user != null && mounted) {
|
||||
|
|
@ -59,23 +51,19 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
if (mounted) {
|
||||
context.read<CustomChangeProvider>().updatePlan(user.subscriptionPlan);
|
||||
await context
|
||||
.read<CustomChangeProvider>()
|
||||
.updatePlan(user.subscriptionPlan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
setUserPlan();
|
||||
globalCallbackAppIsOutdated = () async {
|
||||
context.read<CustomChangeProvider>().updateConnectionState(false);
|
||||
setState(() {
|
||||
appIsOutdated = true;
|
||||
});
|
||||
};
|
||||
Future<void> initAsync() async {
|
||||
await setUserPlan();
|
||||
await apiService.connect(force: true);
|
||||
apiService.listenToNetworkChanges();
|
||||
await apiService.listenToNetworkChanges();
|
||||
// call this function so invalid media files are get purged
|
||||
retryMediaUpload(true);
|
||||
await retryMediaUpload(true);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -97,8 +85,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
globalCallbackConnectionState = (a) {};
|
||||
globalCallbackAppIsOutdated = () {};
|
||||
globalCallbackConnectionState = ({required bool isConnected}) {};
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -145,10 +132,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
themeMode: context.watch<SettingsChangeProvider>().themeMode,
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
"/": (context) =>
|
||||
AppMainWidget(initialPage: 1, appIsOutdated: appIsOutdated),
|
||||
"/chats": (context) =>
|
||||
AppMainWidget(initialPage: 0, appIsOutdated: appIsOutdated)
|
||||
"/": (context) => AppMainWidget(initialPage: 1),
|
||||
"/chats": (context) => AppMainWidget(initialPage: 0)
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -157,17 +142,18 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
class AppMainWidget extends StatefulWidget {
|
||||
const AppMainWidget(
|
||||
{super.key, required this.initialPage, required this.appIsOutdated});
|
||||
const AppMainWidget({
|
||||
super.key,
|
||||
required this.initialPage,
|
||||
});
|
||||
final int initialPage;
|
||||
final bool appIsOutdated;
|
||||
@override
|
||||
State<AppMainWidget> createState() => _AppMainWidgetState();
|
||||
}
|
||||
|
||||
class _AppMainWidgetState extends State<AppMainWidget> {
|
||||
Future<bool> userCreated = isUserCreated();
|
||||
bool showOnboarding = kReleaseMode;
|
||||
bool showOnboarding = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -178,80 +164,25 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: Container());
|
||||
}
|
||||
|
||||
if (snapshot.data!) {
|
||||
} else if (snapshot.data!) {
|
||||
return HomeView(
|
||||
initialPage: widget.initialPage,
|
||||
);
|
||||
}
|
||||
|
||||
return showOnboarding
|
||||
? OnboardingView(
|
||||
callbackOnSuccess: () {
|
||||
setState(() {
|
||||
} else if (showOnboarding) {
|
||||
return OnboardingView(
|
||||
callbackOnSuccess: () => setState(() {
|
||||
showOnboarding = false;
|
||||
});
|
||||
},
|
||||
)
|
||||
: RegisterView(
|
||||
callbackOnSuccess: () {
|
||||
setState(() {
|
||||
}),
|
||||
);
|
||||
}
|
||||
return RegisterView(
|
||||
callbackOnSuccess: () => setState(() {
|
||||
userCreated = isUserCreated();
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.appIsOutdated)
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AppOutdated(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ bool gIsDemoUser = false;
|
|||
// App widget.
|
||||
|
||||
// This callback called by the apiProvider
|
||||
Function(bool) globalCallbackConnectionState = (a) {};
|
||||
Function() globalCallbackAppIsOutdated = () {};
|
||||
void Function({required bool isConnected}) globalCallbackConnectionState = ({
|
||||
required bool isConnected,
|
||||
}) {};
|
||||
void Function() globalCallbackAppIsOutdated = () {};
|
||||
|
||||
bool globalIsAppInBackground = true;
|
||||
int globalBestFriendUserId = -1;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.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/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_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/notifications/setup.notifications.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/storage.dart';
|
||||
|
||||
import 'app.dart';
|
||||
|
||||
void main() async {
|
||||
|
|
@ -31,31 +34,29 @@ void main() async {
|
|||
}
|
||||
|
||||
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 SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
|
||||
setupPushNotification();
|
||||
unawaited(setupPushNotification());
|
||||
|
||||
gCameras = await availableCameras();
|
||||
|
||||
apiService = ApiService();
|
||||
twonlyDB = TwonlyDatabase();
|
||||
|
||||
await twonlyDB.messagesDao.resetPendingDownloadState();
|
||||
await twonlyDB.messagesDao.handleMediaFilesOlderThan7Days();
|
||||
await twonlyDB.signalDao.purgeOutDatedPreKeys();
|
||||
|
||||
// Purge media files in the background
|
||||
purgeReceivedMediaFiles();
|
||||
purgeSendMediaFiles();
|
||||
unawaited(purgeReceivedMediaFiles());
|
||||
unawaited(purgeSendMediaFiles());
|
||||
|
||||
performTwonlySafeBackup();
|
||||
unawaited(performTwonlySafeBackup());
|
||||
|
||||
await initFileDownloader();
|
||||
|
||||
cleanLogFile();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
|
|
@ -63,7 +64,7 @@ void main() async {
|
|||
ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
|
||||
],
|
||||
child: App(),
|
||||
child: const App(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
class SecureStorageKeys {
|
||||
static const String signalIdentity = "signal_identity";
|
||||
static const String signalSignedPreKey = "signed_pre_key_store";
|
||||
static const String apiAuthToken = "api_auth_token";
|
||||
static const String googleFcm = "google_fcm";
|
||||
static const String userData = "userData";
|
||||
static const String twonlySafeLastBackupHash = "twonly_safe_last_backup_hash";
|
||||
static const String signalIdentity = 'signal_identity';
|
||||
static const String signalSignedPreKey = 'signed_pre_key_store';
|
||||
static const String apiAuthToken = 'api_auth_token';
|
||||
static const String googleFcm = 'google_fcm';
|
||||
static const String userData = 'userData';
|
||||
static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash';
|
||||
|
||||
static const String receivingPushKeys = "receiving_push_keys";
|
||||
static const String sendingPushKeys = "sending_push_keys";
|
||||
static const String receivingPushKeys = 'receiving_push_keys';
|
||||
static const String sendingPushKeys = 'sending_push_keys';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,30 +20,33 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
}
|
||||
}
|
||||
|
||||
Future incFlameCounter(
|
||||
int contactId, bool received, DateTime timestamp) async {
|
||||
Contact contact = (await (select(contacts)
|
||||
Future<int> incFlameCounter(
|
||||
int contactId,
|
||||
bool received,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
final contact = (await (select(contacts)
|
||||
..where((t) => t.userId.equals(contactId)))
|
||||
.get())
|
||||
.first;
|
||||
|
||||
int totalMediaCounter = contact.totalMediaCounter + 1;
|
||||
int flameCounter = contact.flameCounter;
|
||||
final totalMediaCounter = contact.totalMediaCounter + 1;
|
||||
var flameCounter = contact.flameCounter;
|
||||
|
||||
if (contact.lastMessageReceived != null &&
|
||||
contact.lastMessageSend != null) {
|
||||
final now = DateTime.now();
|
||||
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) ||
|
||||
contact.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
||||
flameCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Value<DateTime?> lastMessageSend = Value.absent();
|
||||
Value<DateTime?> lastMessageReceived = Value.absent();
|
||||
Value<DateTime?> lastFlameCounterChange = Value.absent();
|
||||
var lastMessageSend = const Value<DateTime?>.absent();
|
||||
var lastMessageReceived = const Value<DateTime?>.absent();
|
||||
var lastFlameCounterChange = const Value<DateTime?>.absent();
|
||||
|
||||
if (contact.lastFlameCounterChange != null) {
|
||||
final now = DateTime.now();
|
||||
|
|
@ -51,7 +54,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
|
||||
if (contact.lastFlameCounterChange!.isBefore(startOfToday)) {
|
||||
// last flame update was yesterday. check if it can be updated.
|
||||
bool updateFlame = false;
|
||||
var updateFlame = false;
|
||||
if (received) {
|
||||
if (contact.lastMessageSend != null &&
|
||||
contact.lastMessageSend!.isAfter(startOfToday)) {
|
||||
|
|
@ -94,11 +97,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
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();
|
||||
}
|
||||
|
||||
Future updateContact(int userId, ContactsCompanion updatedValues) async {
|
||||
Future<void> updateContact(
|
||||
int userId, ContactsCompanion updatedValues) async {
|
||||
await ((update(contacts)..where((c) => c.userId.equals(userId)))
|
||||
.write(updatedValues));
|
||||
if (updatedValues.blocked.present ||
|
||||
|
|
@ -111,7 +115,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
}
|
||||
}
|
||||
|
||||
Future newMessageExchange(int userId) {
|
||||
Future<void> newMessageExchange(int userId) {
|
||||
return updateContact(
|
||||
userId,
|
||||
ContactsCompanion(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class MediaDownloadsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
with _$MediaDownloadsDaoMixin {
|
||||
MediaDownloadsDao(super.db);
|
||||
|
||||
Future updateMediaDownload(
|
||||
Future<void> updateMediaDownload(
|
||||
int messageId, MediaDownloadsCompanion updatedValues) {
|
||||
return (update(mediaDownloads)..where((c) => c.messageId.equals(messageId)))
|
||||
.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)))
|
||||
.go();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class MediaUploadsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
}
|
||||
}
|
||||
|
||||
Future deleteMediaUpload(int mediaUploadId) {
|
||||
Future<void> deleteMediaUpload(int mediaUploadId) {
|
||||
return (delete(mediaUploads)
|
||||
..where((t) => t.mediaUploadId.equals(mediaUploadId)))
|
||||
.go();
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
..where((t) => t.retransmissionId.equals(retransmissionId));
|
||||
}
|
||||
|
||||
Future updateRetransmission(
|
||||
Future<void> updateRetransmission(
|
||||
int retransmissionId,
|
||||
MessageRetransmissionsCompanion updatedValues,
|
||||
) {
|
||||
|
|
@ -54,13 +54,13 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.write(updatedValues);
|
||||
}
|
||||
|
||||
Future resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async {
|
||||
Future<int> resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async {
|
||||
return ((update(messageRetransmissions))
|
||||
..where((m) =>
|
||||
m.contactId.equals(fromUserId) &
|
||||
m.encryptedHash.equals(encryptedHash)))
|
||||
.write(
|
||||
MessageRetransmissionsCompanion(
|
||||
const MessageRetransmissionsCompanion(
|
||||
acknowledgeByServerAt: Value(null),
|
||||
),
|
||||
);
|
||||
|
|
@ -75,13 +75,13 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future deleteRetransmissionById(int retransmissionId) {
|
||||
Future<void> deleteRetransmissionById(int retransmissionId) {
|
||||
return (delete(messageRetransmissions)
|
||||
..where((t) => t.retransmissionId.equals(retransmissionId)))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future deleteRetransmissionByMessageId(int messageId) {
|
||||
Future<void> deleteRetransmissionByMessageId(int messageId) {
|
||||
return (delete(messageRetransmissions)
|
||||
..where((t) => t.messageId.equals(messageId)))
|
||||
.go();
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.watch();
|
||||
}
|
||||
|
||||
Future removeOldMessages() {
|
||||
Future<void> removeOldMessages() {
|
||||
return (update(messages)
|
||||
..where((t) =>
|
||||
(t.openedAt.isSmallerThanValue(
|
||||
|
|
@ -69,7 +69,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.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
|
||||
return (update(messages)
|
||||
..where(
|
||||
|
|
@ -130,7 +130,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.get();
|
||||
}
|
||||
|
||||
Future openedAllNonMediaMessages(int contactId) {
|
||||
Future<void> openedAllNonMediaMessages(int contactId) {
|
||||
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
|
||||
return (update(messages)
|
||||
..where((t) =>
|
||||
|
|
@ -141,7 +141,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.write(updates);
|
||||
}
|
||||
|
||||
Future resetPendingDownloadState() {
|
||||
Future<void> resetPendingDownloadState() {
|
||||
// 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
|
||||
// if they are not yet downloaded...
|
||||
|
|
@ -155,7 +155,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.write(updates);
|
||||
}
|
||||
|
||||
Future openedAllNonMediaMessagesFromOtherUser(int contactId) {
|
||||
Future<void> openedAllNonMediaMessagesFromOtherUser(int contactId) {
|
||||
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
|
||||
return (update(messages)
|
||||
..where((t) =>
|
||||
|
|
@ -167,7 +167,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.write(updates);
|
||||
}
|
||||
|
||||
Future updateMessageByOtherUser(
|
||||
Future<void> updateMessageByOtherUser(
|
||||
int userId, int messageId, MessagesCompanion updatedValues) {
|
||||
return (update(messages)
|
||||
..where((c) =>
|
||||
|
|
@ -175,7 +175,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.write(updatedValues);
|
||||
}
|
||||
|
||||
Future updateMessageByOtherMessageId(
|
||||
Future<void> updateMessageByOtherMessageId(
|
||||
int userId, int messageOtherId, MessagesCompanion updatedValues) {
|
||||
return (update(messages)
|
||||
..where((c) =>
|
||||
|
|
@ -184,7 +184,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.write(updatedValues);
|
||||
}
|
||||
|
||||
Future updateMessageByMessageId(
|
||||
Future<void> updateMessageByMessageId(
|
||||
int messageId, MessagesCompanion updatedValues) {
|
||||
return (update(messages)..where((c) => c.messageId.equals(messageId)))
|
||||
.write(updatedValues);
|
||||
|
|
@ -203,14 +203,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
}
|
||||
}
|
||||
|
||||
Future deleteMessagesByContactId(int contactId) {
|
||||
Future<void> deleteMessagesByContactId(int contactId) {
|
||||
return (delete(messages)
|
||||
..where((t) =>
|
||||
t.contactId.equals(contactId) & t.mediaStored.equals(false)))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future deleteMessagesByContactIdAndOtherMessageId(
|
||||
Future<void> deleteMessagesByContactIdAndOtherMessageId(
|
||||
int contactId, int messageOtherId) {
|
||||
return (delete(messages)
|
||||
..where((t) =>
|
||||
|
|
@ -219,11 +219,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.go();
|
||||
}
|
||||
|
||||
Future deleteMessagesByMessageId(int messageId) {
|
||||
Future<void> deleteMessagesByMessageId(int messageId) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
|
|||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
SignalDao(super.db);
|
||||
Future deleteAllByContactId(int contactId) async {
|
||||
Future<void> deleteAllByContactId(int contactId) async {
|
||||
await (delete(signalContactPreKeys)
|
||||
..where((t) => t.contactId.equals(contactId)))
|
||||
.go();
|
||||
|
|
@ -24,7 +24,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
|
|||
.go();
|
||||
}
|
||||
|
||||
Future deleteAllPreKeysByContactId(int contactId) async {
|
||||
Future<void> deleteAllPreKeysByContactId(int contactId) async {
|
||||
await (delete(signalContactPreKeys)
|
||||
..where((t) => t.contactId.equals(contactId)))
|
||||
.go();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
|
||||
class ConnectIdentityKeyStore extends IdentityKeyStore {
|
||||
|
|
@ -12,13 +12,11 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
|
||||
@override
|
||||
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
|
||||
SignalIdentityKeyStore? identity =
|
||||
await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
|
||||
final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
|
||||
..where((t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName())))
|
||||
.getSingleOrNull();
|
||||
|
||||
if (identity == null) return null;
|
||||
return IdentityKey.fromBytes(identity.identityKey, 0);
|
||||
}
|
||||
|
|
@ -37,7 +35,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
return false;
|
||||
}
|
||||
return trusted == null ||
|
||||
ListEquality().equals(trusted.serialize(), identityKey.serialize());
|
||||
const ListEquality<dynamic>()
|
||||
.equals(trusted.serialize(), identityKey.serialize());
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.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/utils/log.dart';
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ class ConnectPreKeyStore extends PreKeyStore {
|
|||
if (preKeyRecord.isEmpty) {
|
||||
throw InvalidKeyIdException('No such preKey record! - $preKeyId');
|
||||
}
|
||||
Uint8List preKey = preKeyRecord.first.preKey;
|
||||
final preKey = preKeyRecord.first.preKey;
|
||||
return PreKeyRecord.fromBuffer(preKey);
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ class ConnectPreKeyStore extends PreKeyStore {
|
|||
try {
|
||||
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
|
||||
} catch (e) {
|
||||
Log.error("$e");
|
||||
Log.error('$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,24 +7,24 @@ import 'package:twonly/src/constants/secure_storage_keys.dart';
|
|||
|
||||
class ConnectSignedPreKeyStore extends SignedPreKeyStore {
|
||||
Future<HashMap<int, Uint8List>> getStore() async {
|
||||
final storage = FlutterSecureStorage();
|
||||
const storage = FlutterSecureStorage();
|
||||
final storeSerialized = await storage.read(
|
||||
key: SecureStorageKeys.signalSignedPreKey,
|
||||
);
|
||||
var store = HashMap<int, Uint8List>();
|
||||
final store = HashMap<int, Uint8List>();
|
||||
if (storeSerialized == null) {
|
||||
return store;
|
||||
}
|
||||
final storeHashMap = json.decode(storeSerialized);
|
||||
final storeHashMap = json.decode(storeSerialized) as List<List<dynamic>>;
|
||||
for (final item in storeHashMap) {
|
||||
store[item[0]] = base64Decode(item[1]);
|
||||
store[item[0] as int] = base64Decode(item[1] as String);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
Future safeStore(HashMap<int, Uint8List> store) async {
|
||||
final storage = FlutterSecureStorage();
|
||||
var storeHashMap = [];
|
||||
Future<void> safeStore(HashMap<int, Uint8List> store) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final storeHashMap = <List<dynamic>>[];
|
||||
for (final item in store.entries) {
|
||||
storeHashMap.add([item.key, base64Encode(item.value)]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ enum UploadState {
|
|||
readyToUpload,
|
||||
uploadTaskStarted,
|
||||
receiverNotified,
|
||||
// after all users notified all media files that are not storable by the other person will be deleted
|
||||
}
|
||||
|
||||
@DataClassName('MediaUpload')
|
||||
|
|
@ -16,18 +15,30 @@ class MediaUploads extends Table {
|
|||
textEnum<UploadState>().withDefault(Constant(UploadState.pending.name))();
|
||||
|
||||
TextColumn get metadata =>
|
||||
text().map(MediaUploadMetadataConverter()).nullable()();
|
||||
text().map(const MediaUploadMetadataConverter()).nullable()();
|
||||
|
||||
/// exists in UploadState.addedToMessagesDb
|
||||
TextColumn get messageIds => text().map(IntListTypeConverter()).nullable()();
|
||||
|
||||
TextColumn get encryptionData =>
|
||||
text().map(MediaEncryptionDataConverter()).nullable()();
|
||||
text().map(const MediaEncryptionDataConverter()).nullable()();
|
||||
}
|
||||
|
||||
// --- state ----
|
||||
|
||||
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 bool isRealTwonly;
|
||||
late int maxShowTime;
|
||||
|
|
@ -35,8 +46,6 @@ class MediaUploadMetadata {
|
|||
late bool isVideo;
|
||||
late bool mirrorVideo;
|
||||
|
||||
MediaUploadMetadata();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'contactIds': contactIds,
|
||||
|
|
@ -47,28 +56,26 @@ class MediaUploadMetadata {
|
|||
'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 {
|
||||
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> encryptionKey;
|
||||
late List<int> encryptionMac;
|
||||
late List<int> encryptionNonce;
|
||||
|
||||
MediaEncryptionData();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'sha2Hash': sha2Hash,
|
||||
|
|
@ -77,15 +84,6 @@ class MediaEncryptionData {
|
|||
'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 ----
|
||||
|
|
@ -93,7 +91,7 @@ class MediaEncryptionData {
|
|||
class IntListTypeConverter extends TypeConverter<List<int>, String> {
|
||||
@override
|
||||
List<int> fromSql(String fromDb) {
|
||||
return List<int>.from(jsonDecode(fromDb));
|
||||
return List<int>.from(jsonDecode(fromDb) as Iterable<dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart'
|
||||
show driftDatabase, DriftNativeOptions;
|
||||
show DriftNativeOptions, driftDatabase;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
||||
import 'package:twonly/src/database/daos/media_downloads_dao.dart';
|
||||
|
|
@ -73,15 +73,15 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
},
|
||||
onUpgrade: stepByStep(
|
||||
from1To2: (m, schema) async {
|
||||
m.addColumn(schema.messages, schema.messages.errorWhileSending);
|
||||
await m.addColumn(schema.messages, schema.messages.errorWhileSending);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
m.addColumn(schema.contacts, schema.contacts.archived);
|
||||
m.addColumn(
|
||||
await m.addColumn(schema.contacts, schema.contacts.archived);
|
||||
await m.addColumn(
|
||||
schema.contacts, schema.contacts.deleteMessagesAfterXMinutes);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
m.createTable(schema.mediaUploads);
|
||||
await m.createTable(schema.mediaUploads);
|
||||
await m.alterTable(TableMigration(
|
||||
schema.mediaUploads,
|
||||
columnTransformer: {
|
||||
|
|
@ -91,19 +91,19 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
));
|
||||
},
|
||||
from4To5: (m, schema) async {
|
||||
m.createTable(mediaDownloads);
|
||||
m.addColumn(schema.messages, schema.messages.mediaDownloadId);
|
||||
m.addColumn(schema.messages, schema.messages.mediaUploadId);
|
||||
await m.createTable(mediaDownloads);
|
||||
await m.addColumn(schema.messages, schema.messages.mediaDownloadId);
|
||||
await m.addColumn(schema.messages, schema.messages.mediaUploadId);
|
||||
},
|
||||
from5To6: (m, schema) async {
|
||||
m.addColumn(schema.messages, schema.messages.mediaStored);
|
||||
await m.addColumn(schema.messages, schema.messages.mediaStored);
|
||||
},
|
||||
from6To7: (m, schema) async {
|
||||
m.addColumn(schema.contacts, schema.contacts.pinned);
|
||||
await m.addColumn(schema.contacts, schema.contacts.pinned);
|
||||
},
|
||||
from7To8: (m, schema) async {
|
||||
m.addColumn(schema.contacts, schema.contacts.alsoBestFriend);
|
||||
m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
|
||||
await m.addColumn(schema.contacts, schema.contacts.alsoBestFriend);
|
||||
await m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
|
||||
},
|
||||
from8To9: (m, schema) async {
|
||||
await m.alterTable(TableMigration(
|
||||
|
|
@ -115,29 +115,29 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
));
|
||||
},
|
||||
from9To10: (m, schema) async {
|
||||
m.createTable(schema.signalContactPreKeys);
|
||||
m.createTable(schema.signalContactSignedPreKeys);
|
||||
m.addColumn(schema.contacts, schema.contacts.deleted);
|
||||
await m.createTable(schema.signalContactPreKeys);
|
||||
await m.createTable(schema.signalContactSignedPreKeys);
|
||||
await m.addColumn(schema.contacts, schema.contacts.deleted);
|
||||
},
|
||||
from10To11: (m, schema) async {
|
||||
m.createTable(schema.messageRetransmissions);
|
||||
await m.createTable(schema.messageRetransmissions);
|
||||
},
|
||||
from11To12: (m, schema) async {
|
||||
m.addColumn(schema.messageRetransmissions,
|
||||
await m.addColumn(schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.willNotGetACKByUser);
|
||||
},
|
||||
from12To13: (m, schema) async {
|
||||
m.dropColumn(
|
||||
schema.messageRetransmissions, "will_not_get_a_c_k_by_user");
|
||||
await m.dropColumn(
|
||||
schema.messageRetransmissions, 'will_not_get_a_c_k_by_user');
|
||||
},
|
||||
from13To14: (m, schema) async {
|
||||
m.addColumn(schema.messageRetransmissions,
|
||||
await m.addColumn(schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.encryptedHash);
|
||||
},
|
||||
from14To15: (m, schema) async {
|
||||
m.dropColumn(schema.mediaUploads, "upload_tokens");
|
||||
m.dropColumn(schema.mediaUploads, "already_notified");
|
||||
m.addColumn(
|
||||
await m.dropColumn(schema.mediaUploads, 'upload_tokens');
|
||||
await m.dropColumn(schema.mediaUploads, 'already_notified');
|
||||
await m.addColumn(
|
||||
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(messageRetransmissions).go();
|
||||
await delete(mediaDownloads).go();
|
||||
await delete(mediaUploads).go();
|
||||
await update(contacts).write(
|
||||
ContactsCompanion(
|
||||
const ContactsCompanion(
|
||||
avatarSvg: Value(null),
|
||||
myAvatarCounter: Value(0),
|
||||
),
|
||||
|
|
@ -177,7 +177,7 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
await (delete(signalPreKeyStores)
|
||||
..where((t) => (t.createdAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
Duration(days: 25),
|
||||
const Duration(days: 25),
|
||||
),
|
||||
))))
|
||||
.go();
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
"messageSendState_Sending": "Wird gesendet",
|
||||
"messageSendState_TapToLoad": "Tippe zum Laden",
|
||||
"messageSendState_Loading": "Herunterladen",
|
||||
"messageStoredInGalery": "Gespeichert",
|
||||
"messageStoredInGallery": "Gespeichert",
|
||||
"messageReopened": "Erneut geöffnet",
|
||||
"@messageReopened": {},
|
||||
"imageEditorDrawOk": "Zeichnung machen",
|
||||
|
|
@ -329,7 +329,7 @@
|
|||
"appOutdatedBtn": "Jetzt aktualisieren.",
|
||||
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
|
||||
"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",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -145,8 +145,8 @@
|
|||
"@messageSendState_TapToLoad": {},
|
||||
"messageSendState_Loading": "Downloading",
|
||||
"@messageSendState_Loading": {},
|
||||
"messageStoredInGalery": "Stored in gallery",
|
||||
"@messageStoredInGalery": {},
|
||||
"messageStoredInGallery": "Stored in gallery",
|
||||
"@messageStoredInGallery": {},
|
||||
"messageReopened": "Re-opened",
|
||||
"@messageReopened": {},
|
||||
"imageEditorDrawOk": "Take drawing",
|
||||
|
|
@ -486,7 +486,7 @@
|
|||
"appOutdatedBtn": "Update Now",
|
||||
"doubleClickToReopen": "Double-click\nto open again",
|
||||
"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",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -542,11 +542,11 @@ abstract class AppLocalizations {
|
|||
/// **'Downloading'**
|
||||
String get messageSendState_Loading;
|
||||
|
||||
/// No description provided for @messageStoredInGalery.
|
||||
/// No description provided for @messageStoredInGallery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stored in gallery'**
|
||||
String get messageStoredInGalery;
|
||||
String get messageStoredInGallery;
|
||||
|
||||
/// No description provided for @messageReopened.
|
||||
///
|
||||
|
|
@ -2012,11 +2012,11 @@ abstract class AppLocalizations {
|
|||
/// **'Retransmission requested'**
|
||||
String get retransmissionRequested;
|
||||
|
||||
/// No description provided for @testPaymentMethode.
|
||||
/// No description provided for @testPaymentMethod.
|
||||
///
|
||||
/// 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!'**
|
||||
String get testPaymentMethode;
|
||||
String get testPaymentMethod;
|
||||
|
||||
/// No description provided for @testingAccountTitle.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get messageSendState_Loading => 'Herunterladen';
|
||||
|
||||
@override
|
||||
String get messageStoredInGalery => 'Gespeichert';
|
||||
String get messageStoredInGallery => 'Gespeichert';
|
||||
|
||||
@override
|
||||
String get messageReopened => 'Erneut geöffnet';
|
||||
|
|
@ -1068,7 +1068,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get retransmissionRequested => 'Wird erneut versucht.';
|
||||
|
||||
@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!';
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get messageSendState_Loading => 'Downloading';
|
||||
|
||||
@override
|
||||
String get messageStoredInGalery => 'Stored in gallery';
|
||||
String get messageStoredInGallery => 'Stored in gallery';
|
||||
|
||||
@override
|
||||
String get messageReopened => 'Re-opened';
|
||||
|
|
@ -1062,7 +1062,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get retransmissionRequested => 'Retransmission requested';
|
||||
|
||||
@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!';
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: strict_raw_type, prefer_constructors_over_static_methods
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -34,6 +36,14 @@ extension MessageKindExtension on MessageKind {
|
|||
}
|
||||
|
||||
class MessageJson {
|
||||
MessageJson({
|
||||
required this.kind,
|
||||
required this.content,
|
||||
required this.timestamp,
|
||||
this.messageReceiverId,
|
||||
this.messageSenderId,
|
||||
this.retransId,
|
||||
});
|
||||
final MessageKind kind;
|
||||
final MessageContent? content;
|
||||
final int? messageReceiverId;
|
||||
|
|
@ -41,22 +51,13 @@ class MessageJson {
|
|||
int? retransId;
|
||||
DateTime timestamp;
|
||||
|
||||
MessageJson({
|
||||
required this.kind,
|
||||
this.messageReceiverId,
|
||||
this.messageSenderId,
|
||||
this.retransId,
|
||||
required this.content,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
|
||||
}
|
||||
|
||||
static MessageJson fromJson(Map<String, dynamic> json) {
|
||||
final kind = MessageKindExtension.fromString(json["kind"]);
|
||||
final kind = MessageKindExtension.fromString(json['kind'] as String);
|
||||
|
||||
return MessageJson(
|
||||
kind: kind,
|
||||
|
|
@ -65,7 +66,7 @@ class MessageJson {
|
|||
retransId: (json['retransId'] as num?)?.toInt(),
|
||||
content: MessageContent.fromJson(
|
||||
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);
|
||||
case MessageKind.signalDecryptError:
|
||||
return SignalDecryptErrorContent.fromJson(json);
|
||||
default:
|
||||
return null;
|
||||
case MessageKind.storedMediaFile:
|
||||
case MessageKind.contactRequest:
|
||||
case MessageKind.rejectRequest:
|
||||
case MessageKind.acceptRequest:
|
||||
case MessageKind.opened:
|
||||
case MessageKind.requestPushKey:
|
||||
case MessageKind.receiveMediaError:
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Map toJson() {
|
||||
|
|
@ -111,15 +118,6 @@ class 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({
|
||||
required this.maxShowTime,
|
||||
required this.isRealTwonly,
|
||||
|
|
@ -130,25 +128,33 @@ class MediaMessageContent extends MessageContent {
|
|||
this.encryptionMac,
|
||||
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) {
|
||||
return MediaMessageContent(
|
||||
downloadToken: json['downloadToken'] == null
|
||||
? null
|
||||
: List<int>.from(json['downloadToken']),
|
||||
: List<int>.from(json['downloadToken'] as List),
|
||||
encryptionKey: json['encryptionKey'] == null
|
||||
? null
|
||||
: List<int>.from(json['encryptionKey']),
|
||||
: List<int>.from(json['encryptionKey'] as List),
|
||||
encryptionMac: json['encryptionMac'] == null
|
||||
? null
|
||||
: List<int>.from(json['encryptionMac']),
|
||||
: List<int>.from(json['encryptionMac'] as List),
|
||||
encryptionNonce: json['encryptionNonce'] == null
|
||||
? null
|
||||
: List<int>.from(json['encryptionNonce']),
|
||||
maxShowTime: json['maxShowTime'],
|
||||
isRealTwonly: json['isRealTwonly'],
|
||||
isVideo: json['isVideo'] ?? false,
|
||||
mirrorVideo: json['mirrorVideo'] ?? false,
|
||||
: List<int>.from(json['encryptionNonce'] as List),
|
||||
maxShowTime: json['maxShowTime'] as int,
|
||||
isRealTwonly: json['isRealTwonly'] as bool,
|
||||
isVideo: json['isVideo'] as bool? ?? false,
|
||||
mirrorVideo: json['mirrorVideo'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -168,23 +174,23 @@ class MediaMessageContent extends MessageContent {
|
|||
}
|
||||
|
||||
class TextMessageContent extends MessageContent {
|
||||
String text;
|
||||
int? responseToMessageId;
|
||||
int? responseToOtherMessageId;
|
||||
TextMessageContent({
|
||||
required this.text,
|
||||
this.responseToMessageId,
|
||||
this.responseToOtherMessageId,
|
||||
});
|
||||
String text;
|
||||
int? responseToMessageId;
|
||||
int? responseToOtherMessageId;
|
||||
|
||||
static TextMessageContent fromJson(Map json) {
|
||||
return TextMessageContent(
|
||||
text: json['text'],
|
||||
text: json['text'] as String,
|
||||
responseToOtherMessageId: json.containsKey('responseToOtherMessageId')
|
||||
? json['responseToOtherMessageId']
|
||||
? json['responseToOtherMessageId'] as int?
|
||||
: null,
|
||||
responseToMessageId: json.containsKey('responseToMessageId')
|
||||
? json['responseToMessageId']
|
||||
? json['responseToMessageId'] as int?
|
||||
: null);
|
||||
}
|
||||
|
||||
|
|
@ -199,11 +205,11 @@ class TextMessageContent extends MessageContent {
|
|||
}
|
||||
|
||||
class ReopenedMediaFileContent extends MessageContent {
|
||||
int messageId;
|
||||
ReopenedMediaFileContent({required this.messageId});
|
||||
int messageId;
|
||||
|
||||
static ReopenedMediaFileContent fromJson(Map json) {
|
||||
return ReopenedMediaFileContent(messageId: json['messageId']);
|
||||
return ReopenedMediaFileContent(messageId: json['messageId'] as int);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -213,12 +219,12 @@ class ReopenedMediaFileContent extends MessageContent {
|
|||
}
|
||||
|
||||
class SignalDecryptErrorContent extends MessageContent {
|
||||
List<int> encryptedHash;
|
||||
SignalDecryptErrorContent({required this.encryptedHash});
|
||||
List<int> encryptedHash;
|
||||
|
||||
static SignalDecryptErrorContent fromJson(Map json) {
|
||||
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 {
|
||||
AckContent({required this.messageIdToAck, required this.retransIdToAck});
|
||||
int? messageIdToAck;
|
||||
int retransIdToAck;
|
||||
AckContent({required this.messageIdToAck, required this.retransIdToAck});
|
||||
|
||||
static AckContent fromJson(Map json) {
|
||||
return AckContent(
|
||||
messageIdToAck: json['messageIdToAck'],
|
||||
retransIdToAck: json['retransIdToAck'],
|
||||
messageIdToAck: json['messageIdToAck'] as int,
|
||||
retransIdToAck: json['retransIdToAck'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -252,14 +258,14 @@ class AckContent extends MessageContent {
|
|||
}
|
||||
|
||||
class ProfileContent extends MessageContent {
|
||||
ProfileContent({required this.avatarSvg, required this.displayName});
|
||||
String avatarSvg;
|
||||
String displayName;
|
||||
ProfileContent({required this.avatarSvg, required this.displayName});
|
||||
|
||||
static ProfileContent fromJson(Map json) {
|
||||
return ProfileContent(
|
||||
avatarSvg: json['avatarSvg'],
|
||||
displayName: json['displayName'],
|
||||
avatarSvg: json['avatarSvg'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -270,14 +276,14 @@ class ProfileContent extends MessageContent {
|
|||
}
|
||||
|
||||
class PushKeyContent extends MessageContent {
|
||||
PushKeyContent({required this.keyId, required this.key});
|
||||
int keyId;
|
||||
List<int> key;
|
||||
PushKeyContent({required this.keyId, required this.key});
|
||||
|
||||
static PushKeyContent fromJson(Map json) {
|
||||
return PushKeyContent(
|
||||
keyId: json['keyId'],
|
||||
key: List<int>.from(json['key']),
|
||||
keyId: json['keyId'] as int,
|
||||
key: List<int>.from(json['key'] as List),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -291,21 +297,20 @@ class PushKeyContent extends MessageContent {
|
|||
}
|
||||
|
||||
class FlameSyncContent extends MessageContent {
|
||||
int flameCounter;
|
||||
DateTime lastFlameCounterChange;
|
||||
bool bestFriend;
|
||||
|
||||
FlameSyncContent(
|
||||
{required this.flameCounter,
|
||||
required this.bestFriend,
|
||||
required this.lastFlameCounterChange});
|
||||
int flameCounter;
|
||||
DateTime lastFlameCounterChange;
|
||||
bool bestFriend;
|
||||
|
||||
static FlameSyncContent fromJson(Map json) {
|
||||
return FlameSyncContent(
|
||||
flameCounter: json['flameCounter'],
|
||||
bestFriend: json['bestFriend'],
|
||||
lastFlameCounterChange:
|
||||
DateTime.fromMillisecondsSinceEpoch(json['lastFlameCounterChange']),
|
||||
flameCounter: json['flameCounter'] as int,
|
||||
bestFriend: json['bestFriend'] as bool,
|
||||
lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['lastFlameCounterChange'] as int),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
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/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';
|
||||
|
||||
class MemoryItem {
|
||||
|
|
@ -28,45 +29,45 @@ class MemoryItem {
|
|||
static Future<Map<int, MemoryItem>> convertFromMessages(
|
||||
List<Message> messages,
|
||||
) async {
|
||||
Map<int, MemoryItem> items = {};
|
||||
final items = <int, MemoryItem>{};
|
||||
for (final message in messages) {
|
||||
bool isSend = message.messageOtherId == null;
|
||||
int id = message.mediaUploadId ?? message.messageId;
|
||||
final isSend = message.messageOtherId == null;
|
||||
final id = message.mediaUploadId ?? message.messageId;
|
||||
final basePath = await send.getMediaFilePath(
|
||||
isSend ? message.mediaUploadId! : message.messageId,
|
||||
isSend ? "send" : "received",
|
||||
isSend ? 'send' : 'received',
|
||||
);
|
||||
File? imagePath;
|
||||
late File thumbnailFile;
|
||||
File? videoPath;
|
||||
if (await File("$basePath.mp4").exists()) {
|
||||
videoPath = File("$basePath.mp4");
|
||||
if (File('$basePath.mp4').existsSync()) {
|
||||
videoPath = File('$basePath.mp4');
|
||||
thumbnailFile = getThumbnailPath(videoPath);
|
||||
if (!await thumbnailFile.exists()) {
|
||||
if (!thumbnailFile.existsSync()) {
|
||||
await createThumbnailsForVideo(videoPath);
|
||||
}
|
||||
} else if (await File("$basePath.png").exists()) {
|
||||
imagePath = File("$basePath.png");
|
||||
} else if (File('$basePath.png').existsSync()) {
|
||||
imagePath = File('$basePath.png');
|
||||
thumbnailFile = getThumbnailPath(imagePath);
|
||||
if (!await thumbnailFile.exists()) {
|
||||
if (!thumbnailFile.existsSync()) {
|
||||
await createThumbnailsForImage(imagePath);
|
||||
}
|
||||
} else {
|
||||
if (message.mediaStored) {
|
||||
/// media file was deleted, ... remove the file
|
||||
twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
message.messageId,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
mediaStored: Value(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
bool mirrorVideo = false;
|
||||
var mirrorVideo = false;
|
||||
if (videoPath != null) {
|
||||
MediaMessageContent content =
|
||||
MediaMessageContent.fromJson(jsonDecode(message.contentJson!));
|
||||
final content = MediaMessageContent.fromJson(
|
||||
jsonDecode(message.contentJson!) as Map);
|
||||
mirrorVideo = content.mirrorVideo;
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,18 @@
|
|||
// ignore_for_file: avoid_dynamic_calls, strict_raw_type
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.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:package_info_plus/package_info_plus.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'
|
||||
as server;
|
||||
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_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/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/signal/identity.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/fcm.service.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.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';
|
||||
|
||||
final lockConnecting = Mutex();
|
||||
|
|
@ -45,12 +48,12 @@ final lockRetransStore = Mutex();
|
|||
/// It handles errors and does automatically tries to reconnect on
|
||||
/// errors or network changes.
|
||||
class ApiService {
|
||||
final String apiHost = (kDebugMode) ? "10.99.0.140:3030" : "api.twonly.eu";
|
||||
final String apiSecure = (kDebugMode) ? "" : "s";
|
||||
ApiService();
|
||||
final String apiHost = kDebugMode ? '10.99.0.140:3030' : 'api.twonly.eu';
|
||||
final String apiSecure = kDebugMode ? '' : 's';
|
||||
|
||||
bool appIsOutdated = false;
|
||||
bool isAuthenticated = false;
|
||||
ApiService();
|
||||
|
||||
// reconnection params
|
||||
Timer? reconnectionTimer;
|
||||
|
|
@ -69,51 +72,51 @@ class ApiService {
|
|||
_channel = channel;
|
||||
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
|
||||
await _channel!.ready;
|
||||
Log.info("websocket connected to $apiUrl");
|
||||
Log.info('websocket connected to $apiUrl');
|
||||
return true;
|
||||
} on WebSocketChannelException catch (e) {
|
||||
if (!e.message
|
||||
.toString()
|
||||
.contains("No address associated with hostname")) {
|
||||
Log.error("could not connect to api got: $e");
|
||||
.contains('No address associated with hostname')) {
|
||||
Log.error('could not connect to api got: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function is called after the user is authenticated at the server
|
||||
Future onAuthenticated() async {
|
||||
Future<void> onAuthenticated() async {
|
||||
isAuthenticated = true;
|
||||
initFCMAfterAuthenticated();
|
||||
globalCallbackConnectionState(true);
|
||||
await initFCMAfterAuthenticated();
|
||||
globalCallbackConnectionState(isConnected: true);
|
||||
|
||||
if (!globalIsAppInBackground) {
|
||||
retransmitRawBytes();
|
||||
tryTransmitMessages();
|
||||
retryMediaUpload(false);
|
||||
tryDownloadAllMediaFiles();
|
||||
notifyContactsAboutProfileChange();
|
||||
unawaited(retransmitRawBytes());
|
||||
unawaited(tryTransmitMessages());
|
||||
unawaited(retryMediaUpload(false));
|
||||
unawaited(tryDownloadAllMediaFiles());
|
||||
unawaited(notifyContactsAboutProfileChange());
|
||||
twonlyDB.markUpdated();
|
||||
syncFlameCounters();
|
||||
setupNotificationWithUsers();
|
||||
signalHandleNewServerConnection();
|
||||
unawaited(syncFlameCounters());
|
||||
unawaited(setupNotificationWithUsers());
|
||||
unawaited(signalHandleNewServerConnection());
|
||||
}
|
||||
}
|
||||
|
||||
Future onConnected() async {
|
||||
Future<void> onConnected() async {
|
||||
await authenticate();
|
||||
_reconnectionDelay = 5;
|
||||
globalCallbackConnectionState(true);
|
||||
globalCallbackConnectionState(isConnected: true);
|
||||
}
|
||||
|
||||
Future onClosed() async {
|
||||
Future<void> onClosed() async {
|
||||
_channel = null;
|
||||
isAuthenticated = false;
|
||||
globalCallbackConnectionState(false);
|
||||
globalCallbackConnectionState(isConnected: false);
|
||||
await twonlyDB.messagesDao.resetPendingDownloadState();
|
||||
}
|
||||
|
||||
Future startReconnectionTimer() async {
|
||||
Future<void> startReconnectionTimer() async {
|
||||
reconnectionTimer?.cancel();
|
||||
reconnectionTimer ??= Timer(Duration(seconds: _reconnectionDelay), () {
|
||||
reconnectionTimer = null;
|
||||
|
|
@ -122,18 +125,18 @@ class ApiService {
|
|||
_reconnectionDelay += 5;
|
||||
}
|
||||
|
||||
Future close(Function callback) async {
|
||||
Log.info("closing websocket connection");
|
||||
Future<void> close(Function callback) async {
|
||||
Log.info('closing websocket connection');
|
||||
if (_channel != null) {
|
||||
await _channel!.sink.close();
|
||||
onClosed();
|
||||
await onClosed();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
Future listenToNetworkChanges() async {
|
||||
Future<void> listenToNetworkChanges() async {
|
||||
if (connectivitySubscription != null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -155,7 +158,7 @@ class ApiService {
|
|||
reconnectionTimer = null;
|
||||
final user = await getUser();
|
||||
if (user != null && user.isDemoUser) {
|
||||
globalCallbackConnectionState(true);
|
||||
globalCallbackConnectionState(isConnected: true);
|
||||
return false;
|
||||
}
|
||||
return lockConnecting.protect<bool>(() async {
|
||||
|
|
@ -168,9 +171,9 @@ class ApiService {
|
|||
|
||||
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)) {
|
||||
await onConnected();
|
||||
|
|
@ -183,18 +186,18 @@ class ApiService {
|
|||
bool get isConnected => _channel != null && _channel!.closeCode != null;
|
||||
|
||||
void _onDone() {
|
||||
Log.info("websocket closed without error");
|
||||
Log.info('websocket closed without error');
|
||||
onClosed();
|
||||
}
|
||||
|
||||
void _onError(dynamic e) {
|
||||
Log.error("websocket error: $e");
|
||||
Log.error('websocket error: $e');
|
||||
onClosed();
|
||||
}
|
||||
|
||||
void _onData(dynamic msgBuffer) async {
|
||||
try {
|
||||
final msg = server.ServerToClient.fromBuffer(msgBuffer);
|
||||
final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List);
|
||||
if (msg.v0.hasResponse()) {
|
||||
removeFromRetransmissionBuffer(msg.v0.seq);
|
||||
messagesV0[msg.v0.seq] = msg;
|
||||
|
|
@ -202,7 +205,7 @@ class ApiService {
|
|||
await handleServerMessage(msg);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
if (DateTime.now().difference(startTime) > timeout) {
|
||||
Log.error("Timeout for message $seq");
|
||||
Log.error('Timeout for message $seq');
|
||||
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) {
|
||||
_channel!.sink.add(response.writeToBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
var retransmit = await getRetransmission();
|
||||
final retransmit = await getRetransmission();
|
||||
if (retransmit.keys.isEmpty) return;
|
||||
Log.info("retransmitting ${retransmit.keys.length} raw bytes messages");
|
||||
bool gotError = false;
|
||||
Log.info('retransmitting ${retransmit.keys.length} raw bytes messages');
|
||||
var gotError = false;
|
||||
for (final seq in retransmit.keys) {
|
||||
try {
|
||||
_channel!.sink.add(base64Decode(retransmit[seq]));
|
||||
_channel!.sink.add(base64Decode(retransmit[seq] as String));
|
||||
} catch (e) {
|
||||
gotError = true;
|
||||
Log.error("$e");
|
||||
Log.error('$e');
|
||||
}
|
||||
}
|
||||
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 {
|
||||
var retransmit = await getRetransmission();
|
||||
final retransmit = await getRetransmission();
|
||||
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 {
|
||||
var retransmit = await getRetransmission();
|
||||
final retransmit = await getRetransmission();
|
||||
if (retransmit.isEmpty) return;
|
||||
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();
|
||||
|
||||
if (ensureRetransmission) {
|
||||
addToRetransmissionBuffer(seq, requestBytes);
|
||||
await addToRetransmissionBuffer(seq, requestBytes);
|
||||
}
|
||||
|
||||
if (_channel == null) {
|
||||
Log.warn("sending request while api is not connected");
|
||||
Log.warn('sending request while api is not connected');
|
||||
if (!await connect()) {
|
||||
return Result.error(ErrorCode.InternalError);
|
||||
}
|
||||
|
|
@ -302,12 +305,12 @@ class ApiService {
|
|||
|
||||
_channel!.sink.add(requestBytes);
|
||||
|
||||
Result res = asResult(await _waitForResponse(seq));
|
||||
final res = asResult(await _waitForResponse(seq));
|
||||
if (res.isError) {
|
||||
Log.error("got error from server: ${res.error}");
|
||||
Log.error('got error from server: ${res.error}');
|
||||
if (res.error == ErrorCode.AppVersionOutdated) {
|
||||
globalCallbackAppIsOutdated();
|
||||
Log.error("App Version is OUTDATED.");
|
||||
Log.error('App Version is OUTDATED.');
|
||||
appIsOutdated = true;
|
||||
await close(() {});
|
||||
return Result.error(ErrorCode.InternalError);
|
||||
|
|
@ -320,16 +323,16 @@ class ApiService {
|
|||
// this will send the request one more time.
|
||||
return sendRequestSync(request, authenticated: false);
|
||||
} else {
|
||||
Log.error("session is not authenticated");
|
||||
Log.error('session is not authenticated');
|
||||
return Result.error(ErrorCode.InternalError);
|
||||
}
|
||||
}
|
||||
}
|
||||
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(
|
||||
contactId,
|
||||
ContactsCompanion(
|
||||
const ContactsCompanion(
|
||||
deleted: Value(true),
|
||||
),
|
||||
);
|
||||
|
|
@ -339,8 +342,8 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<bool> tryAuthenticateWithToken(int userId) async {
|
||||
final storage = FlutterSecureStorage();
|
||||
String? apiAuthToken =
|
||||
const storage = FlutterSecureStorage();
|
||||
final apiAuthToken =
|
||||
await storage.read(key: SecureStorageKeys.apiAuthToken);
|
||||
|
||||
if (apiAuthToken != null) {
|
||||
|
|
@ -355,21 +358,21 @@ class ApiService {
|
|||
final result = await sendRequestSync(req, authenticated: false);
|
||||
|
||||
if (result.isSuccess) {
|
||||
server.Response_Ok ok = result.value;
|
||||
final ok = result.value as server.Response_Ok;
|
||||
if (ok.hasAuthenticated()) {
|
||||
server.Response_Authenticated authenticated = ok.authenticated;
|
||||
updateUserdata((user) {
|
||||
final authenticated = ok.authenticated;
|
||||
await updateUserdata((user) {
|
||||
user.subscriptionPlan = authenticated.plan;
|
||||
return user;
|
||||
});
|
||||
}
|
||||
Log.info("websocket is authenticated");
|
||||
onAuthenticated();
|
||||
Log.info('websocket is authenticated');
|
||||
unawaited(onAuthenticated());
|
||||
return true;
|
||||
}
|
||||
if (result.isError) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -377,7 +380,7 @@ class ApiService {
|
|||
return false;
|
||||
}
|
||||
|
||||
Future authenticate() async {
|
||||
Future<void> authenticate() async {
|
||||
if (isAuthenticated) return;
|
||||
if (await getSignalIdentity() == null) {
|
||||
return;
|
||||
|
|
@ -392,11 +395,11 @@ class ApiService {
|
|||
|
||||
var handshake = Handshake()
|
||||
..getauthchallenge = Handshake_GetAuthChallenge();
|
||||
var req = createClientToServerFromHandshake(handshake);
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
|
||||
final result = await sendRequestSync(req, authenticated: false);
|
||||
if (result.isError) {
|
||||
Log.error("could not request auth challenge", result);
|
||||
Log.error('could not request auth challenge', result);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -405,7 +408,7 @@ class ApiService {
|
|||
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
|
||||
if (privKey == null) return;
|
||||
final random = getRandomUint8List(32);
|
||||
final signature = sign(privKey.serialize(), challenge, random);
|
||||
final signature = sign(privKey.serialize(), challenge as Uint8List, random);
|
||||
privKey = null;
|
||||
|
||||
final getAuthToken = Handshake_GetAuthToken()
|
||||
|
|
@ -414,18 +417,18 @@ class ApiService {
|
|||
|
||||
final getauthtoken = Handshake()..getauthtoken = getAuthToken;
|
||||
|
||||
var req2 = createClientToServerFromHandshake(getauthtoken);
|
||||
final req2 = createClientToServerFromHandshake(getauthtoken);
|
||||
|
||||
final result2 = await sendRequestSync(req2, authenticated: false);
|
||||
if (result2.isError) {
|
||||
Log.error("could not send auth response: ${result2.error}");
|
||||
Log.error('could not send auth response: ${result2.error}');
|
||||
return;
|
||||
}
|
||||
|
||||
Uint8List apiAuthToken = result2.value.authtoken;
|
||||
String apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||
final apiAuthToken = result2.value.authtoken as Uint8List;
|
||||
final apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||
|
||||
final storage = FlutterSecureStorage();
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.apiAuthToken, value: apiAuthTokenB64);
|
||||
|
||||
|
|
@ -452,51 +455,51 @@ class ApiService {
|
|||
..signedPrekeyId = Int64(signedPreKey.id)
|
||||
..isIos = Platform.isIOS;
|
||||
|
||||
if (inviteCode != null && inviteCode != "") {
|
||||
if (inviteCode != null && inviteCode != '') {
|
||||
register.inviteCode = inviteCode;
|
||||
}
|
||||
|
||||
var handshake = Handshake()..register = register;
|
||||
var req = createClientToServerFromHandshake(handshake);
|
||||
final handshake = Handshake()..register = register;
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
|
||||
return await sendRequestSync(req);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> getUsername(int userId) async {
|
||||
var get = ApplicationData_GetUserById()..userId = Int64(userId);
|
||||
var appData = ApplicationData()..getuserbyid = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req, contactId: userId);
|
||||
final get = ApplicationData_GetUserById()..userId = Int64(userId);
|
||||
final appData = ApplicationData()..getuserbyid = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req, contactId: userId);
|
||||
}
|
||||
|
||||
Future<Result> downloadDone(List<int> token) async {
|
||||
var get = ApplicationData_DownloadDone()..downloadToken = token;
|
||||
var appData = ApplicationData()..downloaddone = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req, ensureRetransmission: true);
|
||||
final get = ApplicationData_DownloadDone()..downloadToken = token;
|
||||
final appData = ApplicationData()..downloaddone = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req, ensureRetransmission: true);
|
||||
}
|
||||
|
||||
Future<Result> getCurrentLocation() async {
|
||||
var get = ApplicationData_GetLocation();
|
||||
var appData = ApplicationData()..getlocation = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final get = ApplicationData_GetLocation();
|
||||
final appData = ApplicationData()..getlocation = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> getUserData(String username) async {
|
||||
var get = ApplicationData_GetUserByUsername()..username = username;
|
||||
var appData = ApplicationData()..getuserbyusername = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final get = ApplicationData_GetUserByUsername()..username = username;
|
||||
final appData = ApplicationData()..getuserbyusername = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Response_PlanBallance?> getPlanBallance() async {
|
||||
var get = ApplicationData_GetCurrentPlanInfos();
|
||||
var appData = ApplicationData()..getcurrentplaninfos = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
Result res = await sendRequestSync(req);
|
||||
final get = ApplicationData_GetCurrentPlanInfos();
|
||||
final appData = ApplicationData()..getcurrentplaninfos = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
final res = await sendRequestSync(req);
|
||||
if (res.isSuccess) {
|
||||
server.Response_Ok ok = res.value;
|
||||
final ok = res.value as server.Response_Ok;
|
||||
if (ok.hasPlanballance()) {
|
||||
return ok.planballance;
|
||||
}
|
||||
|
|
@ -505,12 +508,12 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<Response_Vouchers?> getVoucherList() async {
|
||||
var get = ApplicationData_GetVouchers();
|
||||
var appData = ApplicationData()..getvouchers = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
Result res = await sendRequestSync(req);
|
||||
final get = ApplicationData_GetVouchers();
|
||||
final appData = ApplicationData()..getvouchers = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
final res = await sendRequestSync(req);
|
||||
if (res.isSuccess) {
|
||||
server.Response_Ok ok = res.value;
|
||||
final ok = res.value as server.Response_Ok;
|
||||
if (ok.hasVouchers()) {
|
||||
return ok.vouchers;
|
||||
}
|
||||
|
|
@ -519,12 +522,12 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<List<Response_AddAccountsInvite>?> getAdditionalUserInvites() async {
|
||||
var get = ApplicationData_GetAddAccountsInvites();
|
||||
var appData = ApplicationData()..getaddaccountsinvites = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
Result res = await sendRequestSync(req);
|
||||
final get = ApplicationData_GetAddAccountsInvites();
|
||||
final appData = ApplicationData()..getaddaccountsinvites = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
final res = await sendRequestSync(req);
|
||||
if (res.isSuccess) {
|
||||
server.Response_Ok ok = res.value;
|
||||
final ok = res.value as server.Response_Ok;
|
||||
if (ok.hasAddaccountsinvites()) {
|
||||
return ok.addaccountsinvites.invites;
|
||||
}
|
||||
|
|
@ -533,63 +536,63 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<Result> updatePlanOptions(bool autoRenewal) async {
|
||||
var get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal;
|
||||
var appData = ApplicationData()..updateplanoptions = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal;
|
||||
final appData = ApplicationData()..updateplanoptions = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> removeAdditionalUser(Int64 userId) async {
|
||||
var get = ApplicationData_RemoveAdditionalUser()..userId = userId;
|
||||
var appData = ApplicationData()..removeadditionaluser = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req, contactId: userId.toInt());
|
||||
final get = ApplicationData_RemoveAdditionalUser()..userId = userId;
|
||||
final appData = ApplicationData()..removeadditionaluser = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req, contactId: userId.toInt());
|
||||
}
|
||||
|
||||
Future<Result> buyVoucher(int valueInCents) async {
|
||||
var get = ApplicationData_CreateVoucher()..valueCents = valueInCents;
|
||||
var appData = ApplicationData()..createvoucher = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final get = ApplicationData_CreateVoucher()..valueCents = valueInCents;
|
||||
final appData = ApplicationData()..createvoucher = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> switchToPayedPlan(
|
||||
String planId, bool payMonthly, bool autoRenewal) async {
|
||||
var get = ApplicationData_SwitchToPayedPlan()
|
||||
final get = ApplicationData_SwitchToPayedPlan()
|
||||
..planId = planId
|
||||
..payMonthly = payMonthly
|
||||
..autoRenewal = autoRenewal;
|
||||
var appData = ApplicationData()..switchtopayedplan = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final appData = ApplicationData()..switchtopayedplan = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> redeemVoucher(String voucher) async {
|
||||
var get = ApplicationData_RedeemVoucher()..voucher = voucher;
|
||||
var appData = ApplicationData()..redeemvoucher = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final get = ApplicationData_RedeemVoucher()..voucher = voucher;
|
||||
final appData = ApplicationData()..redeemvoucher = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> deleteAccount() async {
|
||||
var get = ApplicationData_DeleteAccount();
|
||||
var appData = ApplicationData()..deleteaccount = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final get = ApplicationData_DeleteAccount();
|
||||
final appData = ApplicationData()..deleteaccount = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> redeemUserInviteCode(String inviteCode) async {
|
||||
var get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode;
|
||||
var appData = ApplicationData()..redeemadditionalcode = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode;
|
||||
final appData = ApplicationData()..redeemadditionalcode = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> updateFCMToken(String googleFcm) async {
|
||||
var get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm;
|
||||
var appData = ApplicationData()..updategooglefcmtoken = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm;
|
||||
final appData = ApplicationData()..updategooglefcmtoken = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> updateSignedPreKey(
|
||||
|
|
@ -597,22 +600,23 @@ class ApiService {
|
|||
Uint8List signedPreKey,
|
||||
Uint8List signedPreKeySignature,
|
||||
) async {
|
||||
var get = ApplicationData_UpdateSignedPreKey()
|
||||
final get = ApplicationData_UpdateSignedPreKey()
|
||||
..signedPrekeyId = Int64(signedPreKeyId)
|
||||
..signedPrekey = signedPreKey
|
||||
..signedPrekeySignature = signedPreKeySignature;
|
||||
var appData = ApplicationData()..updatesignedprekey = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req);
|
||||
final appData = ApplicationData()..updatesignedprekey = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Response_SignedPreKey?> getSignedKeyByUserId(int userId) async {
|
||||
var get = ApplicationData_GetSignedPreKeyByUserId()..userId = Int64(userId);
|
||||
var appData = ApplicationData()..getsignedprekeybyuserid = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
final get = ApplicationData_GetSignedPreKeyByUserId()
|
||||
..userId = Int64(userId);
|
||||
final appData = ApplicationData()..getsignedprekeybyuserid = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
Result res = await sendRequestSync(req, contactId: userId);
|
||||
if (res.isSuccess) {
|
||||
server.Response_Ok ok = res.value;
|
||||
final ok = res.value as server.Response_Ok;
|
||||
if (ok.hasSignedprekey()) {
|
||||
return ok.signedprekey;
|
||||
}
|
||||
|
|
@ -621,12 +625,12 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<OtherPreKeys?> getPreKeysByUserId(int userId) async {
|
||||
var get = ApplicationData_GetPrekeysByUserId()..userId = Int64(userId);
|
||||
var appData = ApplicationData()..getprekeysbyuserid = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
final get = ApplicationData_GetPrekeysByUserId()..userId = Int64(userId);
|
||||
final appData = ApplicationData()..getprekeysbyuserid = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
Result res = await sendRequestSync(req, contactId: userId);
|
||||
if (res.isSuccess) {
|
||||
server.Response_Ok ok = res.value;
|
||||
final ok = res.value as server.Response_Ok;
|
||||
if (ok.hasUserdata()) {
|
||||
server.Response_UserData data = ok.userdata;
|
||||
if (data.hasSignedPrekey() &&
|
||||
|
|
@ -654,8 +658,8 @@ class ApiService {
|
|||
testMessage.pushData = pushData;
|
||||
}
|
||||
|
||||
var appData = ApplicationData()..textmessage = testMessage;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req, contactId: target);
|
||||
final appData = ApplicationData()..textmessage = testMessage;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req, contactId: target);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:connectivity_plus/connectivity_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:http/http.dart' as http;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.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/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.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/utils/log.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 = {};
|
||||
|
||||
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.
|
||||
downloadStartedForMediaReceived = {};
|
||||
List<Message> messages =
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getAllMessagesPendingDownloading();
|
||||
|
||||
for (Message message in messages) {
|
||||
for (final message in messages) {
|
||||
await startDownloadMedia(message, force);
|
||||
}
|
||||
}
|
||||
|
|
@ -45,8 +47,7 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
|
|||
};
|
||||
|
||||
Future<bool> isAllowedToDownload(bool isVideo) async {
|
||||
final List<ConnectivityResult> connectivityResult =
|
||||
await (Connectivity().checkConnectivity());
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
|
||||
final user = await getUser();
|
||||
final options = user!.autoDownloadOptions ?? defaultAutoDownloadOptions;
|
||||
|
|
@ -76,9 +77,9 @@ Future<bool> isAllowedToDownload(bool isVideo) async {
|
|||
return false;
|
||||
}
|
||||
|
||||
Future handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
||||
int messageId = int.parse(update.task.taskId.replaceAll("download_", ""));
|
||||
bool failed = false;
|
||||
Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
||||
final messageId = int.parse(update.task.taskId.replaceAll('download_', ''));
|
||||
var failed = false;
|
||||
|
||||
if (update.status == TaskStatus.failed ||
|
||||
update.status == TaskStatus.canceled ||
|
||||
|
|
@ -90,46 +91,47 @@ Future handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
|||
} else {
|
||||
failed = true;
|
||||
Log.error(
|
||||
"Got invalid response status code: ${update.responseStatusCode}",
|
||||
'Got invalid response status code: ${update.responseStatusCode}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Log.info("Got ${update.status} for $messageId");
|
||||
Log.info('Got ${update.status} for $messageId');
|
||||
return;
|
||||
}
|
||||
await handleDownloadStatusUpdateInternal(messageId, failed);
|
||||
}
|
||||
|
||||
Future handleDownloadStatusUpdateInternal(int messageId, bool failed) async {
|
||||
Future<void> handleDownloadStatusUpdateInternal(
|
||||
int messageId, bool failed) async {
|
||||
if (failed) {
|
||||
Log.error("Download failed for $messageId");
|
||||
Message? message = await twonlyDB.messagesDao
|
||||
Log.error('Download failed for $messageId');
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(messageId)
|
||||
.getSingleOrNull();
|
||||
if (message != null) {
|
||||
await handleMediaError(message);
|
||||
}
|
||||
} else {
|
||||
Log.info("Download was successfully for $messageId");
|
||||
Log.info('Download was successfully for $messageId');
|
||||
await handleEncryptedFile(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
Future startDownloadMedia(Message message, bool force,
|
||||
Future<void> startDownloadMedia(Message message, bool force,
|
||||
{int retryCounter = 0}) async {
|
||||
if (message.contentJson == null) return;
|
||||
if (downloadStartedForMediaReceived[message.messageId] != null &&
|
||||
retryCounter == 0) {
|
||||
DateTime started = downloadStartedForMediaReceived[message.messageId]!;
|
||||
Duration elapsed = DateTime.now().difference(started);
|
||||
if (elapsed <= Duration(seconds: 60)) {
|
||||
Log.error("Download already started...");
|
||||
final started = downloadStartedForMediaReceived[message.messageId]!;
|
||||
final elapsed = DateTime.now().difference(started);
|
||||
if (elapsed <= const Duration(seconds: 60)) {
|
||||
Log.error('Download already started...');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final content =
|
||||
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
|
||||
final content = MessageContent.fromJson(
|
||||
message.kind, jsonDecode(message.contentJson!) as Map);
|
||||
|
||||
if (content is! MediaMessageContent) return;
|
||||
if (content.downloadToken == null) return;
|
||||
|
|
@ -158,7 +160,7 @@ Future startDownloadMedia(Message message, bool force,
|
|||
if (message.downloadState != DownloadState.downloaded) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
message.messageId,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
downloadState: Value(DownloadState.downloading),
|
||||
),
|
||||
);
|
||||
|
|
@ -166,36 +168,34 @@ Future startDownloadMedia(Message message, bool force,
|
|||
|
||||
downloadStartedForMediaReceived[message.messageId] = DateTime.now();
|
||||
|
||||
String downloadToken = uint8ListToHex(content.downloadToken!);
|
||||
final downloadToken = uint8ListToHex(content.downloadToken!);
|
||||
|
||||
String apiUrl =
|
||||
"http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken";
|
||||
final apiUrl =
|
||||
'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken';
|
||||
|
||||
try {
|
||||
final task = DownloadTask(
|
||||
url: apiUrl,
|
||||
taskId: "download_${media.messageId}",
|
||||
directory: "media/received/",
|
||||
taskId: 'download_${media.messageId}',
|
||||
directory: 'media/received/',
|
||||
baseDirectory: BaseDirectory.applicationSupport,
|
||||
filename: "${media.messageId}.encrypted",
|
||||
filename: '${media.messageId}.encrypted',
|
||||
priority: 0,
|
||||
retries: 10,
|
||||
);
|
||||
|
||||
Log.info(
|
||||
"Got media file. Starting download: ${downloadToken.substring(0, 10)}",
|
||||
'Got media file. Starting download: ${downloadToken.substring(0, 10)}',
|
||||
);
|
||||
|
||||
try {
|
||||
await downloadFileFast(media.messageId, apiUrl);
|
||||
return;
|
||||
} catch (e) {
|
||||
Log.error("Fast download failed: $e");
|
||||
final result = await FileDownloader().enqueue(task);
|
||||
return result;
|
||||
Log.error('Fast download failed: $e');
|
||||
await FileDownloader().enqueue(task);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("Exception during download: $e");
|
||||
Log.error('Exception during download: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,19 +203,19 @@ Future<void> downloadFileFast(
|
|||
int messageId,
|
||||
String apiUrl,
|
||||
) async {
|
||||
final String directoryPath =
|
||||
"${(await getApplicationSupportDirectory()).path}/media/received/";
|
||||
final String filename = "$messageId.encrypted";
|
||||
final directoryPath =
|
||||
'${(await getApplicationSupportDirectory()).path}/media/received/';
|
||||
final filename = '$messageId.encrypted';
|
||||
|
||||
final Directory directory = Directory(directoryPath);
|
||||
if (!await directory.exists()) {
|
||||
final directory = Directory(directoryPath);
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
|
||||
final String filePath = "${directory.path}/$filename";
|
||||
final filePath = '${directory.path}/$filename';
|
||||
|
||||
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) {
|
||||
await File(filePath).writeAsBytes(response.bodyBytes);
|
||||
|
|
@ -228,34 +228,34 @@ Future<void> downloadFileFast(
|
|||
return;
|
||||
}
|
||||
// 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 {
|
||||
Message? msg = await twonlyDB.messagesDao
|
||||
Future<void> handleEncryptedFile(int messageId) async {
|
||||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(messageId)
|
||||
.getSingleOrNull();
|
||||
if (msg == null) {
|
||||
Log.error("Not message for downloaded file found: $messageId");
|
||||
Log.error('Not message for downloaded file found: $messageId');
|
||||
return;
|
||||
}
|
||||
|
||||
Uint8List? encryptedBytes = await readMediaFile(msg.messageId, "encrypted");
|
||||
final encryptedBytes = await readMediaFile(msg.messageId, 'encrypted');
|
||||
|
||||
if (encryptedBytes == null) {
|
||||
Log.error("encrypted bytes are not found for ${msg.messageId}");
|
||||
Log.error('encrypted bytes are not found for ${msg.messageId}');
|
||||
return;
|
||||
}
|
||||
|
||||
MediaMessageContent content =
|
||||
MediaMessageContent.fromJson(jsonDecode(msg.contentJson!));
|
||||
final content =
|
||||
MediaMessageContent.fromJson(jsonDecode(msg.contentJson!) as Map);
|
||||
|
||||
try {
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!);
|
||||
final secretKeyData = SecretKeyData(content.encryptionKey!);
|
||||
|
||||
SecretBox secretBox = SecretBox(
|
||||
final secretBox = SecretBox(
|
||||
encryptedBytes,
|
||||
nonce: content.encryptionNonce!,
|
||||
mac: Mac(content.encryptionMac!),
|
||||
|
|
@ -269,10 +269,10 @@ Future handleEncryptedFile(int messageId) async {
|
|||
if (content.isVideo) {
|
||||
final extractedBytes = extractUint8Lists(imageBytes);
|
||||
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) {
|
||||
// Log.error(
|
||||
// "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;
|
||||
// }
|
||||
} catch (e) {
|
||||
Log.error("$e");
|
||||
Log.error('$e');
|
||||
|
||||
/// legacy support
|
||||
final chacha20 = Xchacha20.poly1305Aead();
|
||||
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!);
|
||||
final secretKeyData = SecretKeyData(content.encryptionKey!);
|
||||
|
||||
SecretBox secretBox = SecretBox(
|
||||
final secretBox = SecretBox(
|
||||
encryptedBytes,
|
||||
nonce: content.encryptionNonce!,
|
||||
mac: Mac(content.encryptionMac!),
|
||||
|
|
@ -300,121 +300,122 @@ Future handleEncryptedFile(int messageId) async {
|
|||
if (content.isVideo) {
|
||||
final extractedBytes = extractUint8Lists(imageBytes);
|
||||
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) {
|
||||
Log.error(
|
||||
"could not decrypt the media file in the second try. reporting error to user: $e");
|
||||
handleMediaError(msg);
|
||||
'could not decrypt the media file in the second try. reporting error to user: $e');
|
||||
await handleMediaError(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
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 {
|
||||
return await readMediaFile(mediaId, "png");
|
||||
return readMediaFile(mediaId, 'png');
|
||||
}
|
||||
|
||||
Future<File?> getVideoPath(int mediaId) async {
|
||||
String basePath = await getMediaFilePath(mediaId, "received");
|
||||
return File("$basePath.mp4");
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
return File('$basePath.mp4');
|
||||
}
|
||||
|
||||
/// --- helper functions ---
|
||||
|
||||
Future<Uint8List?> readMediaFile(int mediaId, String type) async {
|
||||
String basePath = await getMediaFilePath(mediaId, "received");
|
||||
File file = File("$basePath.$type");
|
||||
Log.info("Reading: $file");
|
||||
if (!await file.exists()) {
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
final file = File('$basePath.$type');
|
||||
Log.info('Reading: $file');
|
||||
if (!file.existsSync()) {
|
||||
return null;
|
||||
}
|
||||
return await file.readAsBytes();
|
||||
return file.readAsBytes();
|
||||
}
|
||||
|
||||
Future<bool> existsMediaFile(int mediaId, String type) async {
|
||||
String basePath = await getMediaFilePath(mediaId, "received");
|
||||
File file = File("$basePath.$type");
|
||||
return await file.exists();
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
final file = File('$basePath.$type');
|
||||
return file.existsSync();
|
||||
}
|
||||
|
||||
Future<void> writeMediaFile(int mediaId, String type, Uint8List data) async {
|
||||
String basePath = await getMediaFilePath(mediaId, "received");
|
||||
File file = File("$basePath.$type");
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
final file = File('$basePath.$type');
|
||||
await file.writeAsBytes(data);
|
||||
}
|
||||
|
||||
Future<void> deleteMediaFile(int mediaId, String type) async {
|
||||
String basePath = await getMediaFilePath(mediaId, "received");
|
||||
File file = File("$basePath.$type");
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
final file = File('$basePath.$type');
|
||||
try {
|
||||
if (await file.exists()) {
|
||||
if (file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("Error deleting: $e");
|
||||
Log.error('Error deleting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> purgeReceivedMediaFiles() async {
|
||||
final basedir = await getApplicationSupportDirectory();
|
||||
final directory = Directory(join(basedir.path, 'media', "received"));
|
||||
final directory = Directory(join(basedir.path, 'media', 'received'));
|
||||
await purgeMediaFiles(directory);
|
||||
}
|
||||
|
||||
Future<void> purgeMediaFiles(Directory directory) async {
|
||||
// Check if the directory exists
|
||||
if (await directory.exists()) {
|
||||
if (directory.existsSync()) {
|
||||
// List all files in the directory
|
||||
List<FileSystemEntity> files = directory.listSync();
|
||||
final files = directory.listSync();
|
||||
|
||||
// Iterate over each file
|
||||
for (var file in files) {
|
||||
for (final file in files) {
|
||||
// Get the filename
|
||||
String filename = file.uri.pathSegments.last;
|
||||
final filename = file.uri.pathSegments.last;
|
||||
|
||||
// Use a regular expression to extract the integer part
|
||||
final match = RegExp(r'(\d+)').firstMatch(filename);
|
||||
if (match != null) {
|
||||
// Parse the integer and add it to the list
|
||||
int fileId = int.parse(match.group(0)!);
|
||||
final fileId = int.parse(match.group(0)!);
|
||||
|
||||
try {
|
||||
if (directory.path.endsWith("send")) {
|
||||
List<Message> messages =
|
||||
if (directory.path.endsWith('send')) {
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaUploadId(fileId);
|
||||
bool canBeDeleted = true;
|
||||
var canBeDeleted = true;
|
||||
|
||||
for (final message in messages) {
|
||||
try {
|
||||
MediaMessageContent content = MediaMessageContent.fromJson(
|
||||
jsonDecode(message.contentJson!),
|
||||
final content = MediaMessageContent.fromJson(
|
||||
jsonDecode(message.contentJson!) as Map,
|
||||
);
|
||||
|
||||
DateTime oneDayAgo = DateTime.now().subtract(Duration(days: 1));
|
||||
DateTime twoDaysAgo =
|
||||
DateTime.now().subtract(Duration(days: 1));
|
||||
final oneDayAgo =
|
||||
DateTime.now().subtract(const Duration(days: 1));
|
||||
final twoDaysAgo =
|
||||
DateTime.now().subtract(const Duration(days: 1));
|
||||
|
||||
if (((message.openedAt == null ||
|
||||
if ((message.openedAt == null ||
|
||||
oneDayAgo.isBefore(message.openedAt!)) &&
|
||||
!message.errorWhileSending)) {
|
||||
!message.errorWhileSending) {
|
||||
canBeDeleted = false;
|
||||
} else if (message.mediaStored) {
|
||||
if (!file.path.contains(".original.") &&
|
||||
!file.path.contains(".encrypted")) {
|
||||
if (!file.path.contains('.original.') &&
|
||||
!file.path.contains('.encrypted')) {
|
||||
canBeDeleted = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -429,8 +430,8 @@ Future<void> purgeMediaFiles(Directory directory) async {
|
|||
canBeDeleted = true;
|
||||
}
|
||||
// Encrypted or upload data can be removed when acknowledgeByServer
|
||||
if (file.path.contains(".upload") ||
|
||||
file.path.contains(".encrypted")) {
|
||||
if (file.path.contains('.upload') ||
|
||||
file.path.contains('.encrypted')) {
|
||||
canBeDeleted = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -439,11 +440,11 @@ Future<void> purgeMediaFiles(Directory directory) async {
|
|||
}
|
||||
}
|
||||
if (canBeDeleted) {
|
||||
Log.info("purged media file ${file.path} ");
|
||||
Log.info('purged media file ${file.path} ');
|
||||
file.deleteSync();
|
||||
}
|
||||
} else {
|
||||
Message? message = await twonlyDB.messagesDao
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(fileId)
|
||||
.getSingleOrNull();
|
||||
if ((message == null) ||
|
||||
|
|
@ -455,7 +456,7 @@ Future<void> purgeMediaFiles(Directory directory) async {
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("$e");
|
||||
Log.error('$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:background_downloader/background_downloader.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: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_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
|
@ -35,11 +36,11 @@ import 'package:video_compress/video_compress.dart';
|
|||
Future<ErrorCode?> isAllowedToSend() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return null;
|
||||
if (user.subscriptionPlan == "Preview") {
|
||||
if (user.subscriptionPlan == 'Preview') {
|
||||
return ErrorCode.PlanNotAllowed;
|
||||
}
|
||||
if (user.subscriptionPlan == "Free") {
|
||||
int? todaysImageCounter = user.todaysImageCounter;
|
||||
if (user.subscriptionPlan == 'Free') {
|
||||
var todaysImageCounter = user.todaysImageCounter;
|
||||
if (user.lastImageSend != null && user.todaysImageCounter != null) {
|
||||
if (isToday(user.lastImageSend!)) {
|
||||
if (user.todaysImageCounter == 3) {
|
||||
|
|
@ -53,25 +54,26 @@ Future<ErrorCode?> isAllowedToSend() async {
|
|||
todaysImageCounter = 1;
|
||||
}
|
||||
await updateUserdata((user) {
|
||||
user.lastImageSend = DateTime.now();
|
||||
user.todaysImageCounter = todaysImageCounter;
|
||||
user
|
||||
..lastImageSend = DateTime.now()
|
||||
..todaysImageCounter = todaysImageCounter;
|
||||
return user;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future initFileDownloader() async {
|
||||
Future<void> initFileDownloader() async {
|
||||
FileDownloader().updates.listen((update) async {
|
||||
switch (update) {
|
||||
case TaskStatusUpdate():
|
||||
if (update.task.taskId.contains("upload_")) {
|
||||
if (update.task.taskId.contains('upload_')) {
|
||||
await handleUploadStatusUpdate(update);
|
||||
}
|
||||
if (update.task.taskId.contains("download_")) {
|
||||
if (update.task.taskId.contains('download_')) {
|
||||
await handleDownloadStatusUpdate(update);
|
||||
}
|
||||
if (update.task.taskId.contains("backup")) {
|
||||
if (update.task.taskId.contains('backup')) {
|
||||
await handleBackupStatusUpdate(update);
|
||||
}
|
||||
case TaskProgressUpdate():
|
||||
|
|
@ -82,17 +84,16 @@ Future initFileDownloader() async {
|
|||
|
||||
await FileDownloader().start();
|
||||
|
||||
FileDownloader().configure(androidConfig: [
|
||||
await FileDownloader().configure(androidConfig: [
|
||||
(Config.bypassTLSCertificateValidation, kDebugMode),
|
||||
]);
|
||||
|
||||
if (kDebugMode) {
|
||||
FileDownloader().configureNotification(
|
||||
running: TaskNotification(
|
||||
running: const TaskNotification(
|
||||
'Uploading/Downloading',
|
||||
'{filename} ({progress}).',
|
||||
),
|
||||
complete: null,
|
||||
progressBar: true,
|
||||
);
|
||||
}
|
||||
|
|
@ -112,14 +113,14 @@ Future initFileDownloader() async {
|
|||
|
||||
Future<bool> checkForFailedUploads() async {
|
||||
final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload();
|
||||
List<int> mediaUploadIds = [];
|
||||
for (Message message in messages) {
|
||||
final mediaUploadIds = <int>[];
|
||||
for (var message in messages) {
|
||||
if (mediaUploadIds.contains(message.mediaUploadId)) {
|
||||
continue;
|
||||
}
|
||||
int affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
message.mediaUploadId!,
|
||||
MediaUploadsCompanion(
|
||||
const MediaUploadsCompanion(
|
||||
state: Value(UploadState.pending),
|
||||
encryptionData: Value(
|
||||
null, // start from scratch e.q. encrypt the files again if already happen
|
||||
|
|
@ -128,11 +129,11 @@ Future<bool> checkForFailedUploads() async {
|
|||
);
|
||||
if (affectedRows == 0) {
|
||||
Log.error(
|
||||
"The media from message ${message.messageId} already deleted.",
|
||||
'The media from message ${message.messageId} already deleted.',
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
message.messageId,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
errorWhileSending: Value(true),
|
||||
),
|
||||
);
|
||||
|
|
@ -142,24 +143,24 @@ Future<bool> checkForFailedUploads() async {
|
|||
}
|
||||
if (messages.isNotEmpty) {
|
||||
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
|
||||
}
|
||||
|
||||
final lockingHandleMediaFile = Mutex();
|
||||
Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
|
||||
Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
|
||||
if (maxRetries == 0) {
|
||||
Log.error("retried media upload 3 times. abort retrying");
|
||||
Log.error('retried media upload 3 times. abort retrying');
|
||||
return;
|
||||
}
|
||||
bool retry = await lockingHandleMediaFile.protect<bool>(() async {
|
||||
final retry = await lockingHandleMediaFile.protect<bool>(() async {
|
||||
final mediaFiles = await twonlyDB.mediaUploadsDao.getMediaUploadsForRetry();
|
||||
if (mediaFiles.isEmpty) {
|
||||
return checkForFailedUploads();
|
||||
}
|
||||
Log.info("re uploading ${mediaFiles.length} media files.");
|
||||
Log.info('re uploading ${mediaFiles.length} media files.');
|
||||
for (final mediaFile in mediaFiles) {
|
||||
if (mediaFile.messageIds == null || mediaFile.metadata == null) {
|
||||
if (appRestarted) {
|
||||
|
|
@ -168,7 +169,7 @@ Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
|
|||
await twonlyDB.mediaUploadsDao
|
||||
.deleteMediaUpload(mediaFile.mediaUploadId);
|
||||
Log.info(
|
||||
"upload can be removed, the finalized function was never called...",
|
||||
'upload can be removed, the finalized function was never called...',
|
||||
);
|
||||
}
|
||||
continue;
|
||||
|
|
@ -188,24 +189,23 @@ Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
|
|||
}
|
||||
|
||||
Future<int?> initMediaUpload() async {
|
||||
return await twonlyDB.mediaUploadsDao
|
||||
.insertMediaUpload(MediaUploadsCompanion());
|
||||
return twonlyDB.mediaUploadsDao
|
||||
.insertMediaUpload(const MediaUploadsCompanion());
|
||||
}
|
||||
|
||||
Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
|
||||
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
||||
await videoFilePath.copy("$basePath.original.mp4");
|
||||
return await compressVideoIfExists(mediaUploadId);
|
||||
final basePath = await getMediaFilePath(mediaUploadId, 'send');
|
||||
await videoFilePath.copy('$basePath.original.mp4');
|
||||
return compressVideoIfExists(mediaUploadId);
|
||||
}
|
||||
|
||||
Future<Uint8List> addOrModifyImageToUpload(
|
||||
int mediaUploadId, Uint8List imageBytes) async {
|
||||
Uint8List imageBytesCompressed;
|
||||
|
||||
Stopwatch stopwatch = Stopwatch();
|
||||
stopwatch.start();
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
Log.info("Raw images size in bytes: ${imageBytes.length}");
|
||||
Log.info('Raw images size in bytes: ${imageBytes.length}');
|
||||
|
||||
try {
|
||||
imageBytesCompressed = await FlutterImageCompress.compressWithList(
|
||||
|
|
@ -224,11 +224,11 @@ Future<Uint8List> addOrModifyImageToUpload(
|
|||
quality: 60,
|
||||
);
|
||||
}
|
||||
await writeSendMediaFile(mediaUploadId, "png", imageBytesCompressed);
|
||||
await writeSendMediaFile(mediaUploadId, 'png', imageBytesCompressed);
|
||||
} catch (e) {
|
||||
Log.error("$e");
|
||||
Log.error('$e');
|
||||
// as a fall back use the original image
|
||||
await writeSendMediaFile(mediaUploadId, "png", imageBytes);
|
||||
await writeSendMediaFile(mediaUploadId, 'png', imageBytes);
|
||||
imageBytesCompressed = imageBytes;
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ Future<Uint8List> addOrModifyImageToUpload(
|
|||
|
||||
Log.info(
|
||||
'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.start();
|
||||
|
|
@ -256,16 +256,16 @@ Future<Uint8List> addOrModifyImageToUpload(
|
|||
/// remove the data so it will be done again.
|
||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
mediaUploadId,
|
||||
MediaUploadsCompanion(
|
||||
const MediaUploadsCompanion(
|
||||
encryptionData: Value(null),
|
||||
),
|
||||
);
|
||||
return imageBytesCompressed;
|
||||
}
|
||||
|
||||
Future handlePreProcessingState(MediaUpload media) async {
|
||||
Future<void> handlePreProcessingState(MediaUpload media) async {
|
||||
try {
|
||||
final imageHandler = readSendMediaFile(media.mediaUploadId, "png");
|
||||
final imageHandler = readSendMediaFile(media.mediaUploadId, 'png');
|
||||
final videoHandler = compressVideoIfExists(media.mediaUploadId);
|
||||
await encryptMediaFiles(
|
||||
media.mediaUploadId,
|
||||
|
|
@ -273,34 +273,36 @@ Future handlePreProcessingState(MediaUpload media) async {
|
|||
videoHandler,
|
||||
);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
Future encryptMediaFiles(
|
||||
Future<void> encryptMediaFiles(
|
||||
int mediaUploadId,
|
||||
Future imageHandler,
|
||||
Future<Uint8List> imageHandler,
|
||||
Future<bool>? videoHandler,
|
||||
) async {
|
||||
Log.info("$mediaUploadId encrypting files");
|
||||
Uint8List dataToEncrypt = await imageHandler;
|
||||
Log.info('$mediaUploadId encrypting files');
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
var dataToEncrypt = await imageHandler;
|
||||
|
||||
/// if there is a video wait until it is finished with compression
|
||||
if (videoHandler != null) {
|
||||
if (await videoHandler) {
|
||||
Uint8List compressedVideo = await readSendMediaFile(mediaUploadId, "mp4");
|
||||
final compressedVideo = await readSendMediaFile(mediaUploadId, 'mp4');
|
||||
dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo);
|
||||
}
|
||||
}
|
||||
|
||||
var state = MediaEncryptionData();
|
||||
final state = MediaEncryptionData();
|
||||
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
SecretKeyData secretKey = await (await chacha20.newSecretKey()).extract();
|
||||
final secretKey = await (await chacha20.newSecretKey()).extract();
|
||||
|
||||
state.encryptionKey = secretKey.bytes;
|
||||
state.encryptionNonce = chacha20.newNonce();
|
||||
state
|
||||
..encryptionKey = secretKey.bytes
|
||||
..encryptionNonce = chacha20.newNonce();
|
||||
|
||||
final secretBox = await chacha20.encrypt(
|
||||
dataToEncrypt,
|
||||
|
|
@ -308,46 +310,46 @@ Future encryptMediaFiles(
|
|||
nonce: state.encryptionNonce,
|
||||
);
|
||||
|
||||
state.encryptionMac = secretBox.mac.bytes;
|
||||
|
||||
state.sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes;
|
||||
state
|
||||
..encryptionMac = secretBox.mac.bytes
|
||||
..sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes;
|
||||
|
||||
final encryptedBytes = Uint8List.fromList(secretBox.cipherText);
|
||||
await writeSendMediaFile(
|
||||
mediaUploadId,
|
||||
"encrypted",
|
||||
'encrypted',
|
||||
encryptedBytes,
|
||||
);
|
||||
|
||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
mediaUploadId,
|
||||
MediaUploadsCompanion(
|
||||
state: Value(UploadState.readyToUpload),
|
||||
state: const Value(UploadState.readyToUpload),
|
||||
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 {
|
||||
MediaUploadMetadata metadata = MediaUploadMetadata();
|
||||
metadata.contactIds = contactIds;
|
||||
metadata.isRealTwonly = isRealTwonly;
|
||||
metadata.messageSendAt = DateTime.now();
|
||||
metadata.isVideo = isVideo;
|
||||
metadata.maxShowTime = maxShowTime;
|
||||
metadata.mirrorVideo = mirrorVideo;
|
||||
final metadata = MediaUploadMetadata()
|
||||
..contactIds = contactIds
|
||||
..isRealTwonly = isRealTwonly
|
||||
..messageSendAt = DateTime.now()
|
||||
..isVideo = isVideo
|
||||
..maxShowTime = maxShowTime
|
||||
..mirrorVideo = mirrorVideo;
|
||||
|
||||
List<int> messageIds = [];
|
||||
final messageIds = <int>[];
|
||||
|
||||
for (final contactId in contactIds) {
|
||||
int? messageId = await twonlyDB.messagesDao.insertMessage(
|
||||
final messageId = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
contactId: Value(contactId),
|
||||
kind: Value(MessageKind.media),
|
||||
kind: const Value(MessageKind.media),
|
||||
sendAt: Value(metadata.messageSendAt),
|
||||
downloadState: Value(DownloadState.pending),
|
||||
downloadState: const Value(DownloadState.pending),
|
||||
mediaUploadId: Value(mediaUploadId),
|
||||
contentJson: Value(
|
||||
jsonEncode(
|
||||
|
|
@ -364,14 +366,14 @@ Future finalizeUpload(int mediaUploadId, List<int> contactIds,
|
|||
// de-archive contact when sending a new message
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contactId,
|
||||
ContactsCompanion(
|
||||
const ContactsCompanion(
|
||||
archived: Value(false),
|
||||
),
|
||||
);
|
||||
if (messageId != null) {
|
||||
messageIds.add(messageId);
|
||||
} 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();
|
||||
Future handleNextMediaUploadSteps(int mediaUploadId) async {
|
||||
Future<void> handleNextMediaUploadSteps(int mediaUploadId) async {
|
||||
await lockingHandleNextMediaUploadStep.protect(() async {
|
||||
var mediaUpload = await twonlyDB.mediaUploadsDao
|
||||
final mediaUpload = await twonlyDB.mediaUploadsDao
|
||||
.getMediaUploadById(mediaUploadId)
|
||||
.getSingleOrNull();
|
||||
|
||||
|
|
@ -397,7 +399,7 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
|
|||
if (mediaUpload.state == UploadState.receiverNotified ||
|
||||
mediaUpload.state == UploadState.uploadTaskStarted) {
|
||||
/// Upload done and all users are notified :)
|
||||
Log.info("$mediaUploadId is already done");
|
||||
Log.info('$mediaUploadId is already done');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
|
|
@ -414,7 +416,7 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
|
|||
|
||||
await handleMediaUpload(mediaUpload);
|
||||
} 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);
|
||||
}
|
||||
return false;
|
||||
|
|
@ -427,22 +429,22 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
|
|||
///
|
||||
///
|
||||
|
||||
Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||
bool failed = false;
|
||||
int mediaUploadId = int.parse(update.task.taskId.replaceAll("upload_", ""));
|
||||
Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||
var failed = false;
|
||||
final mediaUploadId = int.parse(update.task.taskId.replaceAll('upload_', ''));
|
||||
|
||||
MediaUpload? media = await twonlyDB.mediaUploadsDao
|
||||
final media = await twonlyDB.mediaUploadsDao
|
||||
.getMediaUploadById(mediaUploadId)
|
||||
.getSingleOrNull();
|
||||
if (media == null) {
|
||||
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;
|
||||
}
|
||||
if (update.status == TaskStatus.failed ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
Log.error("Upload failed: ${update.status}");
|
||||
Log.error('Upload failed: ${update.status}');
|
||||
failed = true;
|
||||
} else if (update.status == TaskStatus.complete) {
|
||||
if (update.responseStatusCode == 200) {
|
||||
|
|
@ -454,7 +456,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
failed = true;
|
||||
}
|
||||
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!) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
messageId,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
acknowledgeByServer: Value(true),
|
||||
errorWhileSending: Value(true),
|
||||
),
|
||||
|
|
@ -474,13 +476,13 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
'Status update for ${update.task.taskId} with status ${update.status}');
|
||||
}
|
||||
|
||||
Future handleUploadSuccess(MediaUpload media) async {
|
||||
Log.info("Upload of ${media.mediaUploadId} success!");
|
||||
Future<void> handleUploadSuccess(MediaUpload media) async {
|
||||
Log.info('Upload of ${media.mediaUploadId} success!');
|
||||
currentUploadTasks.remove(media.mediaUploadId);
|
||||
|
||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
media.mediaUploadId,
|
||||
MediaUploadsCompanion(
|
||||
const MediaUploadsCompanion(
|
||||
state: Value(UploadState.receiverNotified),
|
||||
),
|
||||
);
|
||||
|
|
@ -488,7 +490,7 @@ Future handleUploadSuccess(MediaUpload media) async {
|
|||
for (final messageId in media.messageIds!) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
messageId,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
acknowledgeByServer: Value(true),
|
||||
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 (mediaUpload.messageIds != null) {
|
||||
for (int messageId in mediaUpload.messageIds!) {
|
||||
for (final messageId in mediaUpload.messageIds!) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
messageId,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
errorWhileSending: Value(true),
|
||||
),
|
||||
);
|
||||
|
|
@ -511,20 +513,20 @@ Future handleUploadError(MediaUpload mediaUpload) async {
|
|||
await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId);
|
||||
}
|
||||
|
||||
Future handleMediaUpload(MediaUpload media) async {
|
||||
Uint8List bytesToUpload =
|
||||
await readSendMediaFile(media.mediaUploadId, "encrypted");
|
||||
Future<void> handleMediaUpload(MediaUpload media) async {
|
||||
final bytesToUpload =
|
||||
await readSendMediaFile(media.mediaUploadId, 'encrypted');
|
||||
|
||||
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++) {
|
||||
Message? message = await twonlyDB.messagesDao
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(messageIds[i])
|
||||
.getSingleOrNull();
|
||||
if (message == null) continue;
|
||||
|
|
@ -536,7 +538,7 @@ Future handleMediaUpload(MediaUpload media) async {
|
|||
|
||||
final downloadToken = createDownloadToken();
|
||||
|
||||
MessageJson msg = MessageJson(
|
||||
final msg = MessageJson(
|
||||
kind: MessageKind.media,
|
||||
messageSenderId: messageIds[i],
|
||||
content: MediaMessageContent(
|
||||
|
|
@ -552,19 +554,19 @@ Future handleMediaUpload(MediaUpload media) async {
|
|||
timestamp: media.metadata!.messageSendAt,
|
||||
);
|
||||
|
||||
Uint8List plaintextContent =
|
||||
final plaintextContent =
|
||||
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
|
||||
|
||||
Contact? contact = await twonlyDB.contactsDao
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(message.contactId)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (contact == null || contact.deleted) {
|
||||
Log.warn(
|
||||
"Contact deleted ${message.contactId} or not found in database.");
|
||||
'Contact deleted ${message.contactId} or not found in database.');
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
message.messageId,
|
||||
MessagesCompanion(errorWhileSending: Value(true)),
|
||||
const MessagesCompanion(errorWhileSending: Value(true)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -575,14 +577,14 @@ Future handleMediaUpload(MediaUpload media) async {
|
|||
message.sendAt,
|
||||
);
|
||||
|
||||
Uint8List? encryptedBytes = await signalEncryptMessage(
|
||||
final encryptedBytes = await signalEncryptMessage(
|
||||
message.contactId,
|
||||
plaintextContent,
|
||||
);
|
||||
|
||||
if (encryptedBytes == null) continue;
|
||||
|
||||
var messageOnSuccess = TextMessage()
|
||||
final messageOnSuccess = TextMessage()
|
||||
..body = encryptedBytes
|
||||
..userId = Int64(message.contactId);
|
||||
|
||||
|
|
@ -615,29 +617,29 @@ Future handleMediaUpload(MediaUpload media) async {
|
|||
|
||||
final uploadRequestBytes = uploadRequest.writeToBuffer();
|
||||
|
||||
String? apiAuthTokenRaw =
|
||||
await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken);
|
||||
final apiAuthTokenRaw = await const FlutterSecureStorage()
|
||||
.read(key: SecureStorageKeys.apiAuthToken);
|
||||
if (apiAuthTokenRaw == null) {
|
||||
Log.error("api auth token not defined.");
|
||||
Log.error('api auth token not defined.');
|
||||
return;
|
||||
}
|
||||
String apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
|
||||
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
|
||||
|
||||
File uploadRequestFile = await writeSendMediaFile(
|
||||
final uploadRequestFile = await writeSendMediaFile(
|
||||
media.mediaUploadId,
|
||||
"upload",
|
||||
'upload',
|
||||
uploadRequestBytes,
|
||||
);
|
||||
|
||||
String apiUrl =
|
||||
"http${apiService.apiSecure}://${apiService.apiHost}/api/upload";
|
||||
final apiUrl =
|
||||
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
|
||||
|
||||
try {
|
||||
Log.info("Starting upload from ${media.mediaUploadId}");
|
||||
Log.info('Starting upload from ${media.mediaUploadId}');
|
||||
|
||||
final task = UploadTask.fromFile(
|
||||
taskId: "upload_${media.mediaUploadId}",
|
||||
displayName: (media.metadata?.isVideo ?? false) ? "image" : "video",
|
||||
taskId: 'upload_${media.mediaUploadId}',
|
||||
displayName: (media.metadata?.isVideo ?? false) ? 'image' : 'video',
|
||||
file: uploadRequestFile,
|
||||
url: apiUrl,
|
||||
priority: 0,
|
||||
|
|
@ -652,62 +654,62 @@ Future handleMediaUpload(MediaUpload media) async {
|
|||
try {
|
||||
await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken);
|
||||
} catch (e) {
|
||||
Log.error("Fast upload failed: $e. Using slow method directly.");
|
||||
enqueueUploadTask(media.mediaUploadId);
|
||||
Log.error('Fast upload failed: $e. Using slow method directly.');
|
||||
await enqueueUploadTask(media.mediaUploadId);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("Exception during upload: $e");
|
||||
Log.error('Exception during upload: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Map<int, UploadTask> currentUploadTasks = {};
|
||||
|
||||
Future enqueueUploadTask(int mediaUploadId) async {
|
||||
Future<void> enqueueUploadTask(int mediaUploadId) async {
|
||||
if (currentUploadTasks[mediaUploadId] == null) {
|
||||
Log.info("could not enqueue upload task: $mediaUploadId");
|
||||
Log.info('could not enqueue upload task: $mediaUploadId');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info("Enqueue upload task: $mediaUploadId");
|
||||
Log.info('Enqueue upload task: $mediaUploadId');
|
||||
|
||||
await FileDownloader().enqueue(currentUploadTasks[mediaUploadId]!);
|
||||
currentUploadTasks.remove(mediaUploadId);
|
||||
|
||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
mediaUploadId,
|
||||
MediaUploadsCompanion(
|
||||
const MediaUploadsCompanion(
|
||||
state: Value(UploadState.uploadTaskStarted),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future handleUploadWhenAppGoesBackground() async {
|
||||
Future<void> handleUploadWhenAppGoesBackground() async {
|
||||
if (currentUploadTasks.keys.isEmpty) {
|
||||
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();
|
||||
for (final key in keys) {
|
||||
enqueueUploadTask(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future uploadFileFast(
|
||||
Future<void> uploadFileFast(
|
||||
MediaUpload media,
|
||||
Uint8List uploadRequestFile,
|
||||
String apiUrl,
|
||||
String apiAuthToken,
|
||||
) async {
|
||||
var requestMultipart = http.MultipartRequest(
|
||||
"POST",
|
||||
final requestMultipart = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse(apiUrl),
|
||||
);
|
||||
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
|
||||
|
||||
requestMultipart.files.add(http.MultipartFile.fromBytes(
|
||||
"file",
|
||||
'file',
|
||||
uploadRequestFile,
|
||||
filename: "upload",
|
||||
filename: 'upload',
|
||||
));
|
||||
|
||||
final response = await requestMultipart.send();
|
||||
|
|
@ -721,9 +723,9 @@ Future uploadFileFast(
|
|||
}
|
||||
|
||||
Future<bool> compressVideoIfExists(int mediaUploadId) async {
|
||||
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
||||
File videoOriginalFile = File("$basePath.original.mp4");
|
||||
File videoCompressedFile = File("$basePath.mp4");
|
||||
final basePath = await getMediaFilePath(mediaUploadId, 'send');
|
||||
final videoOriginalFile = File('$basePath.original.mp4');
|
||||
final videoCompressedFile = File('$basePath.mp4');
|
||||
|
||||
if (videoCompressedFile.existsSync()) {
|
||||
// file is already compressed and exists
|
||||
|
|
@ -735,38 +737,35 @@ Future<bool> compressVideoIfExists(int mediaUploadId) async {
|
|||
return false;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = Stopwatch();
|
||||
stopwatch.start();
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
MediaInfo? mediaInfo;
|
||||
try {
|
||||
mediaInfo = await VideoCompress.compressVideo(
|
||||
videoOriginalFile.path,
|
||||
quality: VideoQuality.Res1280x720Quality,
|
||||
deleteOrigin: false,
|
||||
includeAudio:
|
||||
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 the media file is over 20MB compress it with low quality
|
||||
mediaInfo = await VideoCompress.compressVideo(
|
||||
videoOriginalFile.path,
|
||||
quality: VideoQuality.Res960x540Quality,
|
||||
deleteOrigin: false,
|
||||
includeAudio: true,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("during video compression: $e");
|
||||
Log.error('during video compression: $e');
|
||||
}
|
||||
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) {
|
||||
Log.error("could not compress video.");
|
||||
Log.error('could not compress video.');
|
||||
// as a fall back use the non compressed version
|
||||
await videoOriginalFile.copy(videoCompressedFile.path);
|
||||
await videoOriginalFile.delete();
|
||||
|
|
@ -780,26 +779,26 @@ Future<bool> compressVideoIfExists(int mediaUploadId) async {
|
|||
/// --- helper functions ---
|
||||
|
||||
Future<Uint8List> readSendMediaFile(int mediaUploadId, String type) async {
|
||||
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
||||
File file = File("$basePath.$type");
|
||||
if (!await file.exists()) {
|
||||
throw Exception("$file not found");
|
||||
final basePath = await getMediaFilePath(mediaUploadId, 'send');
|
||||
final file = File('$basePath.$type');
|
||||
if (!file.existsSync()) {
|
||||
throw Exception('$file not found');
|
||||
}
|
||||
return await file.readAsBytes();
|
||||
return file.readAsBytes();
|
||||
}
|
||||
|
||||
Future<File> writeSendMediaFile(
|
||||
int mediaUploadId, String type, Uint8List data) async {
|
||||
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
||||
File file = File("$basePath.$type");
|
||||
final basePath = await getMediaFilePath(mediaUploadId, 'send');
|
||||
final file = File('$basePath.$type');
|
||||
await file.writeAsBytes(data);
|
||||
return file;
|
||||
}
|
||||
|
||||
Future<void> deleteSendMediaFile(int mediaUploadId, String type) async {
|
||||
String basePath = await getMediaFilePath(mediaUploadId, "send");
|
||||
File file = File("$basePath.$type");
|
||||
if (await file.exists()) {
|
||||
final basePath = await getMediaFilePath(mediaUploadId, 'send');
|
||||
final file = File('$basePath.$type');
|
||||
if (file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -807,7 +806,7 @@ Future<void> deleteSendMediaFile(int mediaUploadId, String type) async {
|
|||
Future<String> getMediaFilePath(dynamic mediaId, String type) async {
|
||||
final basedir = await getApplicationSupportDirectory();
|
||||
final mediaSendDir = Directory(join(basedir.path, 'media', type));
|
||||
if (!await mediaSendDir.exists()) {
|
||||
if (!mediaSendDir.existsSync()) {
|
||||
await mediaSendDir.create(recursive: true);
|
||||
}
|
||||
return join(mediaSendDir.path, '$mediaId');
|
||||
|
|
@ -816,7 +815,7 @@ Future<String> getMediaFilePath(dynamic mediaId, String type) async {
|
|||
Future<String> getMediaBaseFilePath(String type) async {
|
||||
final basedir = await getApplicationSupportDirectory();
|
||||
final mediaSendDir = Directory(join(basedir.path, 'media', type));
|
||||
if (!await mediaSendDir.exists()) {
|
||||
if (!mediaSendDir.existsSync()) {
|
||||
await mediaSendDir.create(recursive: true);
|
||||
}
|
||||
return mediaSendDir.path;
|
||||
|
|
@ -825,18 +824,15 @@ Future<String> getMediaBaseFilePath(String type) async {
|
|||
/// combines two utf8 list
|
||||
Uint8List combineUint8Lists(Uint8List list1, Uint8List list2) {
|
||||
final combinedLength = 4 + list1.length + list2.length;
|
||||
final combinedList = Uint8List(combinedLength);
|
||||
final byteData = ByteData.sublistView(combinedList);
|
||||
byteData.setInt32(
|
||||
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);
|
||||
final combinedList = Uint8List(combinedLength)
|
||||
..setRange(4, 4 + list1.length, list1)
|
||||
..setRange(4 + list1.length, combinedLength, list2);
|
||||
return combinedList;
|
||||
}
|
||||
|
||||
List<Uint8List> extractUint8Lists(Uint8List 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 list2 = Uint8List.view(combinedList.buffer, 4 + sizeOfList1,
|
||||
combinedList.lengthInBytes - 4 - sizeOfList1);
|
||||
|
|
@ -845,7 +841,7 @@ List<Uint8List> extractUint8Lists(Uint8List combinedList) {
|
|||
|
||||
Future<void> purgeSendMediaFiles() async {
|
||||
final basedir = await getApplicationSupportDirectory();
|
||||
final directory = Directory(join(basedir.path, 'media', "send"));
|
||||
final directory = Directory(join(basedir.path, 'media', 'send'));
|
||||
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)));
|
||||
|
||||
Uint8List createDownloadToken() {
|
||||
final Random random = Random();
|
||||
final random = Random();
|
||||
|
||||
Uint8List token = Uint8List(32);
|
||||
for (int j = 0; j < 32; j++) {
|
||||
final token = Uint8List(32);
|
||||
for (var j = 0; j < 32; j++) {
|
||||
token[j] = random.nextInt(256); // Generate a random byte (0-255)
|
||||
}
|
||||
return token;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:mutex/mutex.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/twonly_database.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/push_notification/push_notification.pb.dart';
|
||||
import 'package:twonly/src/services/api/server_messages.dart'
|
||||
show messageGetsAck;
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -22,12 +21,12 @@ import 'package:twonly/src/utils/storage.dart';
|
|||
|
||||
final lockRetransmission = Mutex();
|
||||
|
||||
Future tryTransmitMessages() async {
|
||||
return await lockRetransmission.protect(() async {
|
||||
Future<void> tryTransmitMessages() async {
|
||||
return lockRetransmission.protect(() async {
|
||||
final retransIds =
|
||||
await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
|
||||
|
||||
Log.info("Retransmitting ${retransIds.length} text messages");
|
||||
Log.info('Retransmitting ${retransIds.length} text messages');
|
||||
|
||||
if (retransIds.isEmpty) return;
|
||||
|
||||
|
|
@ -37,53 +36,51 @@ Future tryTransmitMessages() async {
|
|||
});
|
||||
}
|
||||
|
||||
Future sendRetransmitMessage(int retransId) async {
|
||||
Future<void> sendRetransmitMessage(int retransId) async {
|
||||
try {
|
||||
MessageRetransmission? retrans = await twonlyDB.messageRetransmissionDao
|
||||
final retrans = await twonlyDB.messageRetransmissionDao
|
||||
.getRetransmissionById(retransId)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (retrans == null) {
|
||||
Log.error("$retransId not found in database");
|
||||
Log.error('$retransId not found in database');
|
||||
return;
|
||||
}
|
||||
|
||||
if (retrans.acknowledgeByServerAt != null) {
|
||||
Log.error("$retransId message already retransmitted");
|
||||
Log.error('$retransId message already retransmitted');
|
||||
return;
|
||||
}
|
||||
|
||||
MessageJson json = MessageJson.fromJson(
|
||||
jsonDecode(
|
||||
final json = MessageJson.fromJson(jsonDecode(
|
||||
utf8.decode(
|
||||
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)
|
||||
.getSingleOrNull();
|
||||
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) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
retrans.messageId!,
|
||||
MessagesCompanion(errorWhileSending: Value(true)),
|
||||
const MessagesCompanion(errorWhileSending: Value(true)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Uint8List? encryptedBytes = await signalEncryptMessage(
|
||||
final encryptedBytes = await signalEncryptMessage(
|
||||
retrans.contactId,
|
||||
retrans.plaintextContent,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -96,27 +93,27 @@ Future sendRetransmitMessage(int retransId) async {
|
|||
),
|
||||
);
|
||||
|
||||
Result resp = await apiService.sendTextMessage(
|
||||
final resp = await apiService.sendTextMessage(
|
||||
retrans.contactId,
|
||||
encryptedBytes,
|
||||
retrans.pushData,
|
||||
);
|
||||
|
||||
bool retry = true;
|
||||
var retry = true;
|
||||
|
||||
if (resp.isError) {
|
||||
Log.error("Could not retransmit message.");
|
||||
Log.error('Could not retransmit message.');
|
||||
if (resp.error == ErrorCode.UserIdNotFound) {
|
||||
retry = false;
|
||||
if (retrans.messageId != null) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
retrans.messageId!,
|
||||
MessagesCompanion(errorWhileSending: Value(true)),
|
||||
const MessagesCompanion(errorWhileSending: Value(true)),
|
||||
);
|
||||
}
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
retrans.contactId,
|
||||
ContactsCompanion(deleted: Value(true)),
|
||||
const ContactsCompanion(deleted: Value(true)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +123,7 @@ Future sendRetransmitMessage(int retransId) async {
|
|||
if (retrans.messageId != null) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
retrans.messageId!,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
acknowledgeByServer: Value(true),
|
||||
errorWhileSending: Value(false),
|
||||
),
|
||||
|
|
@ -148,13 +145,13 @@ Future sendRetransmitMessage(int retransId) async {
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("error resending message: $e");
|
||||
Log.error('error resending message: $e');
|
||||
await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId);
|
||||
}
|
||||
}
|
||||
|
||||
// encrypts and stores the message and then sends it in the background
|
||||
Future encryptAndSendMessageAsync(
|
||||
Future<void> encryptAndSendMessageAsync(
|
||||
int? messageId,
|
||||
int userId,
|
||||
MessageJson msg, {
|
||||
|
|
@ -169,7 +166,8 @@ Future encryptAndSendMessageAsync(
|
|||
pushData = await getPushData(userId, pushNotification);
|
||||
}
|
||||
|
||||
int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission(
|
||||
final retransId =
|
||||
await twonlyDB.messageRetransmissionDao.insertRetransmission(
|
||||
MessageRetransmissionsCompanion(
|
||||
contactId: Value(userId),
|
||||
messageId: Value(messageId),
|
||||
|
|
@ -179,13 +177,13 @@ Future encryptAndSendMessageAsync(
|
|||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
msg.retransId = retransId;
|
||||
|
||||
Uint8List plaintextContent =
|
||||
final plaintextContent =
|
||||
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
|
||||
|
||||
await twonlyDB.messageRetransmissionDao.updateRetransmission(
|
||||
|
|
@ -194,29 +192,29 @@ Future encryptAndSendMessageAsync(
|
|||
plaintextContent: Value(plaintextContent)));
|
||||
|
||||
// this can now be done in the background...
|
||||
sendRetransmitMessage(retransId);
|
||||
unawaited(sendRetransmitMessage(retransId));
|
||||
}
|
||||
|
||||
Future sendTextMessage(
|
||||
Future<void> sendTextMessage(
|
||||
int target,
|
||||
TextMessageContent content,
|
||||
PushNotification? pushNotification,
|
||||
) async {
|
||||
DateTime messageSendAt = DateTime.now();
|
||||
final messageSendAt = DateTime.now();
|
||||
DateTime? openedAt;
|
||||
|
||||
if (pushNotification != null && pushNotification.hasReactionContent()) {
|
||||
openedAt = DateTime.now();
|
||||
}
|
||||
|
||||
int? messageId = await twonlyDB.messagesDao.insertMessage(
|
||||
final messageId = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
contactId: Value(target),
|
||||
kind: Value(MessageKind.textMessage),
|
||||
kind: const Value(MessageKind.textMessage),
|
||||
sendAt: Value(messageSendAt),
|
||||
responseToOtherMessageId: Value(content.responseToMessageId),
|
||||
responseToMessageId: Value(content.responseToOtherMessageId),
|
||||
downloadState: Value(DownloadState.downloaded),
|
||||
downloadState: const Value(DownloadState.downloaded),
|
||||
openedAt: Value(openedAt),
|
||||
contentJson: Value(
|
||||
jsonEncode(content.toJson()),
|
||||
|
|
@ -230,7 +228,7 @@ Future sendTextMessage(
|
|||
pushNotification.messageId = Int64(messageId);
|
||||
}
|
||||
|
||||
MessageJson msg = MessageJson(
|
||||
final msg = MessageJson(
|
||||
kind: MessageKind.textMessage,
|
||||
messageSenderId: messageId,
|
||||
content: content,
|
||||
|
|
@ -245,11 +243,11 @@ Future sendTextMessage(
|
|||
);
|
||||
}
|
||||
|
||||
Future notifyContactAboutOpeningMessage(
|
||||
Future<void> notifyContactAboutOpeningMessage(
|
||||
int fromUserId,
|
||||
List<int> messageOtherIds,
|
||||
) async {
|
||||
int biggestMessageId = messageOtherIds.first;
|
||||
var biggestMessageId = messageOtherIds.first;
|
||||
|
||||
for (final messageOtherId in messageOtherIds) {
|
||||
if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId;
|
||||
|
|
@ -267,17 +265,16 @@ Future notifyContactAboutOpeningMessage(
|
|||
await updateLastMessageId(fromUserId, biggestMessageId);
|
||||
}
|
||||
|
||||
Future notifyContactsAboutProfileChange() async {
|
||||
List<Contact> contacts =
|
||||
await twonlyDB.contactsDao.getAllNotBlockedContacts();
|
||||
Future<void> notifyContactsAboutProfileChange() async {
|
||||
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
|
||||
|
||||
UserData? user = await getUser();
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
if (user.avatarSvg == null) return;
|
||||
|
||||
for (Contact contact in contacts) {
|
||||
for (final contact in contacts) {
|
||||
if (contact.myAvatarCounter < user.avatarCounter) {
|
||||
twonlyDB.contactsDao.updateContact(
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contact.userId,
|
||||
ContactsCompanion(
|
||||
myAvatarCounter: Value(user.avatarCounter),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.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/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
|
||||
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/server_to_client.pb.dart'
|
||||
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/messages.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/setup.notifications.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();
|
||||
|
||||
Future handleServerMessage(server.ServerToClient msg) async {
|
||||
Future<void> handleServerMessage(server.ServerToClient msg) async {
|
||||
return lockHandleServerMessage.protect(() async {
|
||||
client.Response? response;
|
||||
|
||||
|
|
@ -39,34 +40,35 @@ Future handleServerMessage(server.ServerToClient msg) async {
|
|||
if (msg.v0.hasRequestNewPreKeys()) {
|
||||
response = await handleRequestNewPreKey();
|
||||
} else if (msg.v0.hasNewMessage()) {
|
||||
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
|
||||
int fromUserId = msg.v0.newMessage.fromUserId.toInt();
|
||||
final body = Uint8List.fromList(msg.v0.newMessage.body);
|
||||
final fromUserId = msg.v0.newMessage.fromUserId.toInt();
|
||||
response = await handleNewMessage(fromUserId, body);
|
||||
} 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;
|
||||
}
|
||||
} catch (e) {
|
||||
response = client.Response()..error = ErrorCode.InternalError;
|
||||
}
|
||||
|
||||
var v0 = client.V0()
|
||||
final v0 = client.V0()
|
||||
..seq = msg.v0.seq
|
||||
..response = response;
|
||||
|
||||
apiService.sendResponse(ClientToServer()..v0 = v0);
|
||||
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
||||
});
|
||||
}
|
||||
|
||||
DateTime lastSignalDecryptMessage = DateTime.now().subtract(Duration(hours: 1));
|
||||
DateTime lastPushKeyRequest = DateTime.now().subtract(Duration(hours: 1));
|
||||
DateTime lastSignalDecryptMessage =
|
||||
DateTime.now().subtract(const Duration(hours: 1));
|
||||
DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
|
||||
|
||||
bool messageGetsAck(MessageKind kind) {
|
||||
return kind != MessageKind.pushKey && kind != MessageKind.ack;
|
||||
}
|
||||
|
||||
Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||
MessageJson? message = await signalDecryptMessage(fromUserId, body);
|
||||
final message = await signalDecryptMessage(fromUserId, body);
|
||||
if (message == null) {
|
||||
final encryptedHash = (await Sha256().hash(body)).bytes;
|
||||
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
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
final ok = client.Response_Ok()..none = true;
|
||||
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) {
|
||||
Log.info("Sending ACK for ${message.kind}");
|
||||
Log.info('Sending ACK for ${message.kind}');
|
||||
|
||||
/// ACK every message
|
||||
await encryptAndSendMessageAsync(
|
||||
|
|
@ -111,7 +113,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
final content = message.content;
|
||||
if (content is AckContent) {
|
||||
if (content.messageIdToAck != null) {
|
||||
final update = MessagesCompanion(
|
||||
const update = MessagesCompanion(
|
||||
acknowledgeByUser: Value(true),
|
||||
errorWhileSending: Value(false),
|
||||
);
|
||||
|
|
@ -125,10 +127,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
await twonlyDB.messageRetransmissionDao
|
||||
.deleteRetransmissionById(content.retransIdToAck);
|
||||
}
|
||||
break;
|
||||
case MessageKind.signalDecryptError:
|
||||
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;
|
||||
if (content is SignalDecryptErrorContent) {
|
||||
|
|
@ -140,16 +141,15 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
final message = await twonlyDB.messageRetransmissionDao
|
||||
.getRetransmissionFromHash(fromUserId, hash);
|
||||
if (message != null) {
|
||||
sendRetransmitMessage(message.retransmissionId);
|
||||
unawaited(sendRetransmitMessage(message.retransmissionId));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case MessageKind.contactRequest:
|
||||
return handleContactRequest(fromUserId, message);
|
||||
|
||||
case MessageKind.flameSync:
|
||||
Contact? contact = await twonlyDB.contactsDao
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
if (contact != null && contact.lastFlameCounterChange != null) {
|
||||
|
|
@ -188,12 +188,12 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
openedMessage.mediaRetransmissionState ==
|
||||
MediaRetransmitting.none &&
|
||||
openedMessage.sendAt
|
||||
.isAfter(DateTime.now().subtract(Duration(days: 2)))) {
|
||||
.isAfter(DateTime.now().subtract(const Duration(days: 2)))) {
|
||||
// reset the media upload state to pending,
|
||||
// this will cause the media to be re-encrypted again
|
||||
twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
openedMessage.mediaUploadId!,
|
||||
MediaUploadsCompanion(
|
||||
const MediaUploadsCompanion(
|
||||
state: Value(
|
||||
UploadState.pending,
|
||||
),
|
||||
|
|
@ -203,18 +203,18 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
await twonlyDB.messagesDao.updateMessageByOtherUser(
|
||||
fromUserId,
|
||||
message.messageReceiverId!,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
downloadState: Value(DownloadState.pending),
|
||||
mediaRetransmissionState:
|
||||
Value(MediaRetransmitting.retransmitted),
|
||||
),
|
||||
);
|
||||
retryMediaUpload(false);
|
||||
unawaited(retryMediaUpload(false));
|
||||
} else {
|
||||
await twonlyDB.messagesDao.updateMessageByOtherUser(
|
||||
fromUserId,
|
||||
message.messageReceiverId!,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
errorWhileSending: Value(true),
|
||||
),
|
||||
);
|
||||
|
|
@ -226,7 +226,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
if (message.messageReceiverId != null) {
|
||||
final update = MessagesCompanion(
|
||||
openedAt: Value(message.timestamp),
|
||||
errorWhileSending: Value(false),
|
||||
errorWhileSending: const Value(false),
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageByOtherUser(
|
||||
fromUserId,
|
||||
|
|
@ -243,20 +243,17 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageKind.rejectRequest:
|
||||
await deleteContact(fromUserId);
|
||||
break;
|
||||
|
||||
case MessageKind.acceptRequest:
|
||||
final update = ContactsCompanion(accepted: Value(true));
|
||||
const update = ContactsCompanion(accepted: Value(true));
|
||||
await twonlyDB.contactsDao.updateContact(fromUserId, update);
|
||||
notifyContactsAboutProfileChange();
|
||||
break;
|
||||
unawaited(notifyContactsAboutProfileChange());
|
||||
|
||||
case MessageKind.profileChange:
|
||||
var content = message.content;
|
||||
final content = message.content;
|
||||
if (content is ProfileContent) {
|
||||
final update = ContactsCompanion(
|
||||
avatarSvg: Value(content.avatarSvg),
|
||||
|
|
@ -264,14 +261,13 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
);
|
||||
await twonlyDB.contactsDao.updateContact(fromUserId, update);
|
||||
}
|
||||
createPushAvatars();
|
||||
break;
|
||||
unawaited(createPushAvatars());
|
||||
|
||||
case MessageKind.requestPushKey:
|
||||
if (lastPushKeyRequest
|
||||
.isBefore(DateTime.now().subtract(Duration(seconds: 60)))) {
|
||||
.isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) {
|
||||
lastPushKeyRequest = DateTime.now();
|
||||
setupNotificationWithUsers(forceContact: fromUserId);
|
||||
unawaited(setupNotificationWithUsers(forceContact: fromUserId));
|
||||
}
|
||||
|
||||
case MessageKind.pushKey:
|
||||
|
|
@ -282,14 +278,15 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
}
|
||||
}
|
||||
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
if (message.kind != MessageKind.textMessage &&
|
||||
message.kind != MessageKind.media &&
|
||||
message.kind != MessageKind.storedMediaFile &&
|
||||
message.kind != MessageKind.reopenedMedia) {
|
||||
Log.error("Got unknown MessageKind $message");
|
||||
Log.error('Got unknown MessageKind $message');
|
||||
} else if (message.messageSenderId == null) {
|
||||
Log.error("Messageid not defined $message");
|
||||
Log.error('Messageid not defined $message');
|
||||
} else {
|
||||
if (message.kind == MessageKind.storedMediaFile) {
|
||||
if (message.messageReceiverId != null) {
|
||||
|
|
@ -297,7 +294,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
await twonlyDB.messagesDao.updateMessageByOtherUser(
|
||||
fromUserId,
|
||||
message.messageReceiverId!,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
mediaStored: Value(true),
|
||||
errorWhileSending: Value(false),
|
||||
),
|
||||
|
|
@ -308,11 +305,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
.getSingleOrNull();
|
||||
if (msg != null && msg.mediaUploadId != null) {
|
||||
final filePath =
|
||||
await getMediaFilePath(msg.mediaUploadId, "send");
|
||||
if (filePath.contains("mp4")) {
|
||||
createThumbnailsForVideo(File(filePath));
|
||||
await getMediaFilePath(msg.mediaUploadId, 'send');
|
||||
if (filePath.contains('mp4')) {
|
||||
unawaited(createThumbnailsForVideo(File(filePath)));
|
||||
} else {
|
||||
createThumbnailsForImage(File(filePath));
|
||||
unawaited(createThumbnailsForImage(File(filePath)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -330,8 +327,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
.deleteMessagesByMessageId(openedMessage.messageId);
|
||||
} else {
|
||||
Log.error(
|
||||
"Got a duplicated message from other user: ${message.messageSenderId!}");
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
'Got a duplicated message from other user: ${message.messageSenderId!}');
|
||||
final ok = client.Response_Ok()..none = true;
|
||||
return client.Response()..ok = ok;
|
||||
}
|
||||
}
|
||||
|
|
@ -340,7 +337,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
int? responseToOtherMessageId;
|
||||
int? messageId;
|
||||
|
||||
bool acknowledgeByUser = false;
|
||||
var acknowledgeByUser = false;
|
||||
DateTime? openedAt;
|
||||
|
||||
if (message.kind == MessageKind.reopenedMedia) {
|
||||
|
|
@ -369,7 +366,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
fromUserId,
|
||||
responseToMessageId,
|
||||
MessagesCompanion(
|
||||
errorWhileSending: Value(false),
|
||||
errorWhileSending: const Value(false),
|
||||
openedAt: Value(
|
||||
DateTime.now(),
|
||||
), // 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(
|
||||
contactId: Value(fromUserId),
|
||||
kind: Value(message.kind),
|
||||
messageOtherId: Value(message.messageSenderId),
|
||||
contentJson: Value(contentJson),
|
||||
acknowledgeByServer: Value(true),
|
||||
acknowledgeByServer: const Value(true),
|
||||
acknowledgeByUser: Value(acknowledgeByUser),
|
||||
responseToMessageId: Value(responseToMessageId),
|
||||
responseToOtherMessageId: Value(responseToOtherMessageId),
|
||||
|
|
@ -403,7 +400,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
}
|
||||
|
||||
if (message.kind == MessageKind.media) {
|
||||
twonlyDB.contactsDao.incFlameCounter(
|
||||
await twonlyDB.contactsDao.incFlameCounter(
|
||||
fromUserId,
|
||||
true,
|
||||
message.timestamp,
|
||||
|
|
@ -413,37 +410,37 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
.getMessageByMessageId(messageId)
|
||||
.getSingleOrNull();
|
||||
if (msg != null) {
|
||||
startDownloadMedia(msg, false);
|
||||
unawaited(startDownloadMedia(msg, false));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.error("Content is not defined $message");
|
||||
Log.error('Content is not defined $message');
|
||||
}
|
||||
|
||||
// unarchive contact when receiving a new message
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
fromUserId,
|
||||
ContactsCompanion(
|
||||
const ContactsCompanion(
|
||||
archived: Value(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
final ok = client.Response_Ok()..none = true;
|
||||
return client.Response()..ok = ok;
|
||||
}
|
||||
|
||||
Future<client.Response> handleRequestNewPreKey() async {
|
||||
List<PreKeyRecord> localPreKeys = await signalGetPreKeys();
|
||||
final localPreKeys = await signalGetPreKeys();
|
||||
|
||||
List<client.Response_PreKey> prekeysList = [];
|
||||
for (int i = 0; i < localPreKeys.length; i++) {
|
||||
final prekeysList = <client.Response_PreKey>[];
|
||||
for (var i = 0; i < localPreKeys.length; i++) {
|
||||
prekeysList.add(client.Response_PreKey()
|
||||
..id = Int64(localPreKeys[i].id)
|
||||
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
|
||||
}
|
||||
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
|
||||
var ok = client.Response_Ok()..prekeys = prekeys;
|
||||
final prekeys = client.Response_Prekeys(prekeys: prekeysList);
|
||||
final ok = client.Response_Ok()..prekeys = prekeys;
|
||||
return client.Response()..ok = ok;
|
||||
}
|
||||
|
||||
|
|
@ -451,18 +448,18 @@ Future<client.Response> handleContactRequest(
|
|||
int fromUserId, MessageJson message) async {
|
||||
// request the username by the server so an attacker can not
|
||||
// forge the displayed username in the contact request
|
||||
Result username = await apiService.getUsername(fromUserId);
|
||||
final username = await apiService.getUsername(fromUserId);
|
||||
if (username.isSuccess) {
|
||||
Uint8List name = username.value.userdata.username;
|
||||
final name = username.value.userdata.username as Uint8List;
|
||||
await twonlyDB.contactsDao.insertContact(
|
||||
ContactsCompanion(
|
||||
username: Value(utf8.decode(name)),
|
||||
userId: Value(fromUserId),
|
||||
requested: Value(true),
|
||||
requested: const Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
await setupNotificationWithUsers();
|
||||
var ok = client.Response_Ok()..none = true;
|
||||
final ok = client.Response_Ok()..none = true;
|
||||
return client.Response()..ok = ok;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,17 @@ import 'package:twonly/src/services/api/messages.dart';
|
|||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
|
||||
class Result<T, E> {
|
||||
Result.error(this.error) : value = null;
|
||||
Result.success(this.value) : error = null;
|
||||
|
||||
final T? value;
|
||||
final E? error;
|
||||
|
||||
bool get isSuccess => value != 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) {
|
||||
if (msg == null) {
|
||||
return Result.error(ErrorCode.InternalError);
|
||||
|
|
@ -44,20 +45,20 @@ ClientToServer createClientToServerFromHandshake(Handshake handshake) {
|
|||
|
||||
ClientToServer createClientToServerFromApplicationData(
|
||||
ApplicationData applicationData) {
|
||||
var v0 = client.V0()
|
||||
final v0 = client.V0()
|
||||
..seq = Int64(0)
|
||||
..applicationdata = applicationData;
|
||||
return ClientToServer()..v0 = v0;
|
||||
}
|
||||
|
||||
Future deleteContact(int contactId) async {
|
||||
Future<void> deleteContact(int contactId) async {
|
||||
await twonlyDB.messagesDao.deleteAllMessagesByContactId(contactId);
|
||||
await twonlyDB.signalDao.deleteAllByContactId(contactId);
|
||||
await deleteSessionWithTarget(contactId);
|
||||
await twonlyDB.contactsDao.deleteContactByUserId(contactId);
|
||||
}
|
||||
|
||||
Future rejectUser(int contactId) async {
|
||||
Future<void> rejectUser(int contactId) async {
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
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(
|
||||
message.messageId,
|
||||
MessagesCompanion(
|
||||
const MessagesCompanion(
|
||||
errorWhileSending: Value(true),
|
||||
mediaRetransmissionState: Value(
|
||||
MediaRetransmitting.requested,
|
||||
|
|
@ -80,7 +81,7 @@ Future handleMediaError(Message message) async {
|
|||
),
|
||||
);
|
||||
if (message.messageOtherId != null) {
|
||||
encryptAndSendMessageAsync(
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
message.contactId,
|
||||
MessageJson(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.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/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import '../../firebase_options.dart';
|
||||
|
||||
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
||||
|
||||
Future initFCMAfterAuthenticated() async {
|
||||
Future<void> initFCMAfterAuthenticated() async {
|
||||
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 {
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
if (fcmToken == null) {
|
||||
Log.error("Error getting fcmToken");
|
||||
Log.error('Error getting fcmToken');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -33,14 +35,14 @@ Future initFCMAfterAuthenticated() async {
|
|||
await apiService.updateFCMToken(fcmToken);
|
||||
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
|
||||
}).onError((err) {
|
||||
Log.error("could not listen on token refresh");
|
||||
Log.error('could not listen on token refresh');
|
||||
});
|
||||
} 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(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
|
@ -51,15 +53,7 @@ Future initFCMService() async {
|
|||
// of notifications they would like to receive once the user receives a notification.
|
||||
// final notificationSettings =
|
||||
// await FirebaseMessaging.instance.requestPermission(provisional: true);
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
announcement: false,
|
||||
badge: true,
|
||||
carPlay: false,
|
||||
criticalAlert: false,
|
||||
provisional: false,
|
||||
sound: true,
|
||||
);
|
||||
await FirebaseMessaging.instance.requestPermission();
|
||||
|
||||
// For apple platforms, ensure the APNS token is available before making any FCM plugin API calls
|
||||
if (Platform.isIOS) {
|
||||
|
|
@ -69,9 +63,7 @@ Future initFCMService() async {
|
|||
}
|
||||
}
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
handleRemoteMessage(message);
|
||||
});
|
||||
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
|
|
@ -80,19 +72,19 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|||
Log.info('Handling a background message: ${message.messageId}');
|
||||
await handleRemoteMessage(message);
|
||||
// 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) {
|
||||
Log.error("Got message in Dart while on iOS");
|
||||
Log.error('Got message in Dart while on iOS');
|
||||
}
|
||||
|
||||
if (message.notification != null) {
|
||||
String title = message.notification!.title ?? "";
|
||||
String body = message.notification!.body ?? "";
|
||||
final title = message.notification!.title ?? '';
|
||||
final body = message.notification!.body ?? '';
|
||||
await customLocalPushNotification(title, body);
|
||||
} else if (message.data["push_data"] != null) {
|
||||
await handlePushData(message.data["push_data"]);
|
||||
} else if (message.data['push_data'] != null) {
|
||||
await handlePushData(message.data['push_data'] as String);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/model/json/message.dart' as my;
|
||||
|
||||
Future syncFlameCounters() async {
|
||||
Future<void> syncFlameCounters() async {
|
||||
var user = await getUser();
|
||||
if (user == null) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ import 'package:twonly/src/utils/log.dart';
|
|||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Future customLocalPushNotification(String title, String msg) async {
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
Future<void> customLocalPushNotification(String title, String msg) async {
|
||||
const androidNotificationDetails = AndroidNotificationDetails(
|
||||
'1',
|
||||
'System',
|
||||
channelDescription: 'System messages.',
|
||||
|
|
@ -24,9 +23,8 @@ Future customLocalPushNotification(String title, String msg) async {
|
|||
priority: Priority.max,
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails darwinNotificationDetails =
|
||||
DarwinNotificationDetails();
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
const darwinNotificationDetails = DarwinNotificationDetails();
|
||||
const notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
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 {
|
||||
final pushData =
|
||||
EncryptedPushNotification.fromBuffer(base64.decode(pushDataB64));
|
||||
|
|
@ -48,7 +46,7 @@ Future handlePushData(String pushDataB64) async {
|
|||
PushUser? foundPushUser;
|
||||
|
||||
if (pushData.keyId == 0) {
|
||||
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
|
||||
final key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
|
||||
pushNotification = await tryDecryptMessage(key, pushData);
|
||||
} else {
|
||||
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
|
|
@ -70,14 +68,14 @@ Future handlePushData(String pushDataB64) async {
|
|||
if (pushNotification != null) {
|
||||
if (pushNotification.kind == PushKind.testNotification) {
|
||||
await customLocalPushNotification(
|
||||
"Test notification",
|
||||
"This is a test notification.",
|
||||
'Test notification',
|
||||
'This is a test notification.',
|
||||
);
|
||||
} else if (foundPushUser != null) {
|
||||
if (pushNotification.hasMessageId()) {
|
||||
if (pushNotification.messageId <= foundPushUser.lastMessageId) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -90,8 +88,8 @@ Future handlePushData(String pushDataB64) async {
|
|||
}
|
||||
} catch (e) {
|
||||
await customLocalPushNotification(
|
||||
"Du hast eine neue Nachricht.",
|
||||
"Öffne twonly um mehr zu erfahren.",
|
||||
'Du hast eine neue Nachricht.',
|
||||
'Öffne twonly um mehr zu erfahren.',
|
||||
);
|
||||
Log.error(e);
|
||||
}
|
||||
|
|
@ -101,9 +99,9 @@ Future<PushNotification?> tryDecryptMessage(
|
|||
List<int> key, EncryptedPushNotification push) async {
|
||||
try {
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
SecretKeyData secretKeyData = SecretKeyData(key);
|
||||
final secretKeyData = SecretKeyData(key);
|
||||
|
||||
SecretBox secretBox = SecretBox(
|
||||
final secretBox = SecretBox(
|
||||
push.ciphertext,
|
||||
nonce: push.nonce,
|
||||
mac: Mac(push.mac),
|
||||
|
|
@ -118,7 +116,7 @@ Future<PushNotification?> tryDecryptMessage(
|
|||
}
|
||||
}
|
||||
|
||||
Future showLocalPushNotification(
|
||||
Future<void> showLocalPushNotification(
|
||||
PushUser pushUser,
|
||||
PushNotification pushNotification,
|
||||
) async {
|
||||
|
|
@ -127,24 +125,23 @@ Future showLocalPushNotification(
|
|||
|
||||
// do not show notification for blocked users...
|
||||
if (pushUser.blocked) {
|
||||
Log.info("Blocked a message from a blocked user!");
|
||||
Log.info('Blocked a message from a blocked user!');
|
||||
return;
|
||||
}
|
||||
|
||||
title = pushUser.displayName;
|
||||
body = getPushNotificationText(pushNotification);
|
||||
if (body == "") {
|
||||
Log.error("No push notification type defined!");
|
||||
if (body == '') {
|
||||
Log.error('No push notification type defined!');
|
||||
}
|
||||
|
||||
FilePathAndroidBitmap? styleInformation;
|
||||
String? avatarPath = await getAvatarIcon(pushUser.userId.toInt());
|
||||
final avatarPath = await getAvatarIcon(pushUser.userId.toInt());
|
||||
if (avatarPath != null) {
|
||||
styleInformation = FilePathAndroidBitmap(avatarPath);
|
||||
}
|
||||
|
||||
AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
final androidNotificationDetails = AndroidNotificationDetails(
|
||||
'0',
|
||||
'Messages',
|
||||
channelDescription: 'Messages from other users.',
|
||||
|
|
@ -154,9 +151,8 @@ Future showLocalPushNotification(
|
|||
largeIcon: styleInformation,
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails darwinNotificationDetails =
|
||||
DarwinNotificationDetails();
|
||||
NotificationDetails notificationDetails = NotificationDetails(
|
||||
const darwinNotificationDetails = DarwinNotificationDetails();
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
iOS: darwinNotificationDetails,
|
||||
);
|
||||
|
|
@ -170,19 +166,18 @@ Future showLocalPushNotification(
|
|||
);
|
||||
}
|
||||
|
||||
Future showLocalPushNotificationWithoutUserId(
|
||||
Future<void> showLocalPushNotificationWithoutUserId(
|
||||
PushNotification pushNotification,
|
||||
) async {
|
||||
String? title;
|
||||
String? body;
|
||||
|
||||
body = getPushNotificationTextWithoutUserId(pushNotification.kind);
|
||||
if (body == "") {
|
||||
Log.error("No push notification type defined!");
|
||||
if (body == '') {
|
||||
Log.error('No push notification type defined!');
|
||||
}
|
||||
|
||||
AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
const androidNotificationDetails = AndroidNotificationDetails(
|
||||
'0',
|
||||
'Messages',
|
||||
channelDescription: 'Messages from other users.',
|
||||
|
|
@ -191,9 +186,8 @@ Future showLocalPushNotificationWithoutUserId(
|
|||
ticker: 'You got a new message.',
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails darwinNotificationDetails =
|
||||
DarwinNotificationDetails();
|
||||
NotificationDetails notificationDetails = NotificationDetails(
|
||||
const darwinNotificationDetails = DarwinNotificationDetails();
|
||||
const notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails, iOS: darwinNotificationDetails);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
|
|
@ -219,99 +213,99 @@ Future<String?> getAvatarIcon(int contactId) async {
|
|||
String getPushNotificationTextWithoutUserId(PushKind pushKind) {
|
||||
Map<String, String> pushNotificationText;
|
||||
|
||||
String systemLanguage = Platform.localeName;
|
||||
final systemLanguage = Platform.localeName;
|
||||
|
||||
if (systemLanguage.contains("de")) {
|
||||
if (systemLanguage.contains('de')) {
|
||||
pushNotificationText = {
|
||||
PushKind.text.name: "Du hast eine neue Nachricht erhalten.",
|
||||
PushKind.twonly.name: "Du hast ein neues twonly erhalten.",
|
||||
PushKind.video.name: "Du hast ein neues Video erhalten.",
|
||||
PushKind.image.name: "Du hast ein neues Bild erhalten.",
|
||||
PushKind.text.name: 'Du hast eine neue Nachricht erhalten.',
|
||||
PushKind.twonly.name: 'Du hast ein neues twonly erhalten.',
|
||||
PushKind.video.name: 'Du hast ein neues Video erhalten.',
|
||||
PushKind.image.name: 'Du hast ein neues Bild erhalten.',
|
||||
PushKind.contactRequest.name:
|
||||
"Du hast eine neue Kontaktanfrage erhalten.",
|
||||
PushKind.acceptRequest.name: "Deine Kontaktanfrage wurde angenommen.",
|
||||
PushKind.storedMediaFile.name: "Dein Bild wurde gespeichert.",
|
||||
PushKind.reaction.name: "Du hast eine Reaktion auf dein Bild erhalten.",
|
||||
PushKind.reopenedMedia.name: "Dein Bild wurde erneut geöffnet.",
|
||||
'Du hast eine neue Kontaktanfrage erhalten.',
|
||||
PushKind.acceptRequest.name: 'Deine Kontaktanfrage wurde angenommen.',
|
||||
PushKind.storedMediaFile.name: 'Dein Bild wurde gespeichert.',
|
||||
PushKind.reaction.name: 'Du hast eine Reaktion auf dein Bild erhalten.',
|
||||
PushKind.reopenedMedia.name: 'Dein Bild wurde erneut geöffnet.',
|
||||
PushKind.reactionToVideo.name:
|
||||
"Du hast eine Reaktion auf dein Video erhalten.",
|
||||
'Du hast eine Reaktion auf dein Video erhalten.',
|
||||
PushKind.reactionToText.name:
|
||||
"Du hast eine Reaktion auf deinen Text erhalten.",
|
||||
'Du hast eine Reaktion auf deinen Text erhalten.',
|
||||
PushKind.reactionToImage.name:
|
||||
"Du hast eine Reaktion auf dein Bild erhalten.",
|
||||
PushKind.response.name: "Du hast eine Antwort erhalten.",
|
||||
'Du hast eine Reaktion auf dein Bild erhalten.',
|
||||
PushKind.response.name: 'Du hast eine Antwort erhalten.',
|
||||
};
|
||||
} else {
|
||||
pushNotificationText = {
|
||||
PushKind.text.name: "You have received a new message.",
|
||||
PushKind.twonly.name: "You have received a new twonly.",
|
||||
PushKind.video.name: "You have received a new video.",
|
||||
PushKind.image.name: "You have received a new image.",
|
||||
PushKind.contactRequest.name: "You have received a new contact request.",
|
||||
PushKind.acceptRequest.name: "Your contact request has been accepted.",
|
||||
PushKind.storedMediaFile.name: "Your image has been saved.",
|
||||
PushKind.reaction.name: "You have received a reaction to your image.",
|
||||
PushKind.reopenedMedia.name: "Your image has been reopened.",
|
||||
PushKind.text.name: 'You have received a new message.',
|
||||
PushKind.twonly.name: 'You have received a new twonly.',
|
||||
PushKind.video.name: 'You have received a new video.',
|
||||
PushKind.image.name: 'You have received a new image.',
|
||||
PushKind.contactRequest.name: 'You have received a new contact request.',
|
||||
PushKind.acceptRequest.name: 'Your contact request has been accepted.',
|
||||
PushKind.storedMediaFile.name: 'Your image has been saved.',
|
||||
PushKind.reaction.name: 'You have received a reaction to your image.',
|
||||
PushKind.reopenedMedia.name: 'Your image has been reopened.',
|
||||
PushKind.reactionToVideo.name:
|
||||
"You have received a reaction to your video.",
|
||||
'You have received a reaction to your video.',
|
||||
PushKind.reactionToText.name:
|
||||
"You have received a reaction to your text.",
|
||||
'You have received a reaction to your text.',
|
||||
PushKind.reactionToImage.name:
|
||||
"You have received a reaction to your image.",
|
||||
PushKind.response.name: "You have received a response.",
|
||||
'You have received a reaction to your image.',
|
||||
PushKind.response.name: 'You have received a response.',
|
||||
};
|
||||
}
|
||||
return pushNotificationText[pushKind.name] ?? "";
|
||||
return pushNotificationText[pushKind.name] ?? '';
|
||||
}
|
||||
|
||||
String getPushNotificationText(PushNotification pushNotification) {
|
||||
String systemLanguage = Platform.localeName;
|
||||
final systemLanguage = Platform.localeName;
|
||||
|
||||
Map<String, String> pushNotificationText;
|
||||
|
||||
if (systemLanguage.contains("de")) {
|
||||
if (systemLanguage.contains('de')) {
|
||||
pushNotificationText = {
|
||||
PushKind.text.name: "hat dir eine Nachricht gesendet.",
|
||||
PushKind.twonly.name: "hat dir ein twonly gesendet.",
|
||||
PushKind.video.name: "hat dir ein Video gesendet.",
|
||||
PushKind.image.name: "hat dir ein Bild gesendet.",
|
||||
PushKind.contactRequest.name: "möchte sich mit dir vernetzen.",
|
||||
PushKind.acceptRequest.name: "ist jetzt mit dir vernetzt.",
|
||||
PushKind.storedMediaFile.name: "hat dein Bild gespeichert.",
|
||||
PushKind.reaction.name: "hat auf dein Bild reagiert.",
|
||||
PushKind.reopenedMedia.name: "hat dein Bild erneut geöffnet.",
|
||||
PushKind.text.name: 'hat dir eine Nachricht gesendet.',
|
||||
PushKind.twonly.name: 'hat dir ein twonly gesendet.',
|
||||
PushKind.video.name: 'hat dir ein Video gesendet.',
|
||||
PushKind.image.name: 'hat dir ein Bild gesendet.',
|
||||
PushKind.contactRequest.name: 'möchte sich mit dir vernetzen.',
|
||||
PushKind.acceptRequest.name: 'ist jetzt mit dir vernetzt.',
|
||||
PushKind.storedMediaFile.name: 'hat dein Bild gespeichert.',
|
||||
PushKind.reaction.name: 'hat auf dein Bild reagiert.',
|
||||
PushKind.reopenedMedia.name: 'hat dein Bild erneut geöffnet.',
|
||||
PushKind.reactionToVideo.name:
|
||||
"hat mit {{reaction}} auf dein Video reagiert.",
|
||||
'hat mit {{reaction}} auf dein Video reagiert.',
|
||||
PushKind.reactionToText.name:
|
||||
"hat mit {{reaction}} auf deine Nachricht reagiert.",
|
||||
'hat mit {{reaction}} auf deine Nachricht reagiert.',
|
||||
PushKind.reactionToImage.name:
|
||||
"hat mit {{reaction}} auf dein Bild reagiert.",
|
||||
PushKind.response.name: "hat dir geantwortet.",
|
||||
'hat mit {{reaction}} auf dein Bild reagiert.',
|
||||
PushKind.response.name: 'hat dir geantwortet.',
|
||||
};
|
||||
} else {
|
||||
pushNotificationText = {
|
||||
PushKind.text.name: "has sent you a message.",
|
||||
PushKind.twonly.name: "has sent you a twonly.",
|
||||
PushKind.video.name: "has sent you a video.",
|
||||
PushKind.image.name: "has sent you an image.",
|
||||
PushKind.contactRequest.name: "wants to connect with you.",
|
||||
PushKind.acceptRequest.name: "is now connected with you.",
|
||||
PushKind.storedMediaFile.name: "has stored your image.",
|
||||
PushKind.reaction.name: "has reacted to your image.",
|
||||
PushKind.reopenedMedia.name: "has reopened your image.",
|
||||
PushKind.text.name: 'has sent you a message.',
|
||||
PushKind.twonly.name: 'has sent you a twonly.',
|
||||
PushKind.video.name: 'has sent you a video.',
|
||||
PushKind.image.name: 'has sent you an image.',
|
||||
PushKind.contactRequest.name: 'wants to connect with you.',
|
||||
PushKind.acceptRequest.name: 'is now connected with you.',
|
||||
PushKind.storedMediaFile.name: 'has stored your image.',
|
||||
PushKind.reaction.name: 'has reacted to your image.',
|
||||
PushKind.reopenedMedia.name: 'has reopened your image.',
|
||||
PushKind.reactionToVideo.name:
|
||||
"has reacted with {{reaction}} to your video.",
|
||||
'has reacted with {{reaction}} to your video.',
|
||||
PushKind.reactionToText.name:
|
||||
"has reacted with {{reaction}} to your message.",
|
||||
'has reacted with {{reaction}} to your message.',
|
||||
PushKind.reactionToImage.name:
|
||||
"has reacted with {{reaction}} to your image.",
|
||||
PushKind.response.name: "has responded.",
|
||||
'has reacted with {{reaction}} to your image.',
|
||||
PushKind.response.name: 'has responded.',
|
||||
};
|
||||
}
|
||||
var contentText = pushNotificationText[pushNotification.kind.name] ?? "";
|
||||
var contentText = pushNotificationText[pushNotification.kind.name] ?? '';
|
||||
if (pushNotification.hasReactionContent()) {
|
||||
contentText = contentText.replaceAll(
|
||||
"{{reaction}}", pushNotification.reactionContent);
|
||||
'{{reaction}}', pushNotification.reactionContent);
|
||||
}
|
||||
return contentText;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,20 +18,20 @@ import 'package:twonly/src/services/api/messages.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
/// This function must be called after the database is setup
|
||||
Future setupNotificationWithUsers(
|
||||
Future<void> setupNotificationWithUsers(
|
||||
{bool force = false, int? forceContact}) async {
|
||||
var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
|
||||
// HotFIX: Search for user with id 0 if not there remove all
|
||||
// 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) {
|
||||
Log.info("Clearing push keys");
|
||||
Log.info('Clearing push keys');
|
||||
await setPushKeys(SecureStorageKeys.receivingPushKeys, []);
|
||||
pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
pushUsers.add(PushUser(
|
||||
userId: Int64(0),
|
||||
displayName: "NoUser",
|
||||
pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys)
|
||||
..add(PushUser(
|
||||
userId: Int64(),
|
||||
displayName: 'NoUser',
|
||||
pushKeys: [],
|
||||
));
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ Future setupNotificationWithUsers(
|
|||
|
||||
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
|
||||
for (final contact in contacts) {
|
||||
PushUser? pushUser =
|
||||
final pushUser =
|
||||
pushUsers.firstWhereOrNull((x) => x.userId == contact.userId);
|
||||
|
||||
if (pushUser != null) {
|
||||
|
|
@ -67,11 +67,11 @@ Future setupNotificationWithUsers(
|
|||
pushUser.pushKeys.add(lastKey);
|
||||
pushUser.pushKeys.add(pushKey);
|
||||
wasChanged = true;
|
||||
Log.info("Creating new pushkey for ${contact.userId}");
|
||||
Log.info('Creating new pushkey for ${contact.userId}');
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
|
||||
|
|
@ -87,7 +87,6 @@ Future setupNotificationWithUsers(
|
|||
displayName: getContactDisplayName(contact),
|
||||
blocked: contact.blocked,
|
||||
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(
|
||||
null,
|
||||
userId,
|
||||
|
|
@ -114,11 +113,10 @@ Future sendNewPushKey(int userId, PushKey pushKey) async {
|
|||
);
|
||||
}
|
||||
|
||||
Future updatePushUser(Contact contact) async {
|
||||
var pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
Future<void> updatePushUser(Contact contact) async {
|
||||
final pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
|
||||
PushUser? pushUser =
|
||||
pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
|
||||
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
|
||||
|
||||
if (pushUser == null) {
|
||||
pushKeys.add(PushUser(
|
||||
|
|
@ -126,20 +124,21 @@ Future updatePushUser(Contact contact) async {
|
|||
displayName: getContactDisplayName(contact),
|
||||
pushKeys: [],
|
||||
blocked: contact.blocked,
|
||||
lastMessageId: Int64(0),
|
||||
lastMessageId: Int64(),
|
||||
));
|
||||
} else {
|
||||
pushUser.displayName = getContactDisplayName(contact);
|
||||
pushUser.blocked = contact.blocked;
|
||||
pushUser
|
||||
..displayName = getContactDisplayName(contact)
|
||||
..blocked = contact.blocked;
|
||||
}
|
||||
|
||||
await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys);
|
||||
}
|
||||
|
||||
Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
|
||||
var pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
|
||||
Future<void> handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
|
||||
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) {
|
||||
final contact = await twonlyDB.contactsDao
|
||||
|
|
@ -151,13 +150,13 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
|
|||
displayName: getContactDisplayName(contact),
|
||||
pushKeys: [],
|
||||
blocked: contact.blocked,
|
||||
lastMessageId: Int64(0),
|
||||
lastMessageId: Int64(),
|
||||
));
|
||||
pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
|
||||
}
|
||||
|
||||
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...
|
||||
|
|
@ -173,14 +172,12 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
|
|||
await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys);
|
||||
}
|
||||
|
||||
Future updateLastMessageId(int fromUserId, int messageId) async {
|
||||
List<PushUser> pushUsers =
|
||||
await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
Future<void> updateLastMessageId(int fromUserId, int messageId) async {
|
||||
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
|
||||
PushUser? pushUser =
|
||||
pushUsers.firstWhereOrNull((x) => x.userId == fromUserId);
|
||||
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == fromUserId);
|
||||
if (pushUser == null) {
|
||||
setupNotificationWithUsers();
|
||||
unawaited(setupNotificationWithUsers());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -193,13 +190,12 @@ Future updateLastMessageId(int fromUserId, int messageId) async {
|
|||
/// this will trigger a push notification
|
||||
/// push notification only containing the message kind and username
|
||||
Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
|
||||
final List<PushUser> pushKeys =
|
||||
await getPushKeys(SecureStorageKeys.sendingPushKeys);
|
||||
final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
|
||||
|
||||
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
|
||||
int keyId = 0;
|
||||
var key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
|
||||
var keyId = 0;
|
||||
|
||||
PushUser? pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId);
|
||||
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId);
|
||||
|
||||
if (pushUser == null) {
|
||||
// user does not have send any push keys
|
||||
|
|
@ -210,7 +206,7 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
|
|||
content.kind != PushKind.testNotification) {
|
||||
// this will be enforced after every app uses this system... :/
|
||||
// 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(
|
||||
null,
|
||||
toUserId,
|
||||
|
|
@ -226,7 +222,7 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
|
|||
key = pushUser.pushKeys.last.key;
|
||||
keyId = pushUser.pushKeys.last.id.toInt();
|
||||
} catch (e) {
|
||||
Log.error("No push notification key found for user $toUserId");
|
||||
Log.error('No push notification key found for user $toUserId');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -248,39 +244,36 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
|
|||
}
|
||||
|
||||
Future<List<PushUser>> getPushKeys(String storageKey) async {
|
||||
var storage = FlutterSecureStorage();
|
||||
String? pushKeysProto = await storage.read(
|
||||
const storage = FlutterSecureStorage();
|
||||
final pushKeysProto = await storage.read(
|
||||
key: storageKey,
|
||||
iOptions: IOSOptions(
|
||||
groupId: "CN332ZUGRP.eu.twonly.shared",
|
||||
synchronizable: false,
|
||||
iOptions: const IOSOptions(
|
||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
if (pushKeysProto == null) return [];
|
||||
Uint8List pushKeysRaw = base64Decode(pushKeysProto);
|
||||
final pushKeysRaw = base64Decode(pushKeysProto);
|
||||
return PushUsers.fromBuffer(pushKeysRaw).users;
|
||||
}
|
||||
|
||||
Future setPushKeys(String storageKey, List<PushUser> pushKeys) async {
|
||||
var storage = FlutterSecureStorage();
|
||||
Future<void> setPushKeys(String storageKey, List<PushUser> pushKeys) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
await storage.delete(
|
||||
key: storageKey,
|
||||
iOptions: IOSOptions(
|
||||
groupId: "CN332ZUGRP.eu.twonly.shared",
|
||||
synchronizable: false,
|
||||
iOptions: const IOSOptions(
|
||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
|
||||
String jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer());
|
||||
final jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer());
|
||||
await storage.write(
|
||||
key: storageKey,
|
||||
value: jsonString,
|
||||
iOptions: IOSOptions(
|
||||
groupId: "CN332ZUGRP.eu.twonly.shared",
|
||||
synchronizable: false,
|
||||
iOptions: const IOSOptions(
|
||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Future<void> setupPushNotification() async {
|
|||
);
|
||||
}
|
||||
|
||||
Future createPushAvatars() async {
|
||||
Future<void> createPushAvatars() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return; // avatars currently only shown in Android...
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.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/database/signal/connect_signal_protocol_store.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/utils.signal.dart';
|
||||
|
|
@ -17,19 +16,18 @@ final lockingSignalEncryption = Mutex();
|
|||
|
||||
Future<Uint8List?> signalEncryptMessage(
|
||||
int target, Uint8List plaintextContent) async {
|
||||
return await lockingSignalEncryption.protect<Uint8List?>(() async {
|
||||
return lockingSignalEncryption.protect<Uint8List?>(() async {
|
||||
try {
|
||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||
final signalStore = (await getSignalStore())!;
|
||||
final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
|
||||
|
||||
SessionCipher session = SessionCipher.fromStore(signalStore, address);
|
||||
final session = SessionCipher.fromStore(signalStore, address);
|
||||
|
||||
SignalContactPreKey? preKey = await getPreKeyByContactId(target);
|
||||
SignalContactSignedPreKey? signedPreKey =
|
||||
await getSignedPreKeyByContactId(target);
|
||||
final preKey = await getPreKeyByContactId(target);
|
||||
final signedPreKey = await getSignedPreKeyByContactId(target);
|
||||
|
||||
if (signedPreKey != null) {
|
||||
SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore(
|
||||
final sessionBuilder = SessionBuilder.fromSignalStore(
|
||||
signalStore,
|
||||
address,
|
||||
);
|
||||
|
|
@ -45,20 +43,20 @@ Future<Uint8List?> signalEncryptMessage(
|
|||
);
|
||||
}
|
||||
|
||||
ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint(
|
||||
final ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint(
|
||||
DjbECPublicKey(Uint8List.fromList(signedPreKey.signedPreKey))
|
||||
.serialize(),
|
||||
1,
|
||||
);
|
||||
|
||||
Uint8List? tempSignedPreKeySignature = Uint8List.fromList(
|
||||
final Uint8List? tempSignedPreKeySignature = Uint8List.fromList(
|
||||
signedPreKey.signedPreKeySignature,
|
||||
);
|
||||
|
||||
final IdentityKey? tempIdentityKey =
|
||||
await signalStore.getIdentity(address);
|
||||
if (tempIdentityKey != null) {
|
||||
PreKeyBundle preKeyBundle = PreKeyBundle(
|
||||
final preKeyBundle = PreKeyBundle(
|
||||
target,
|
||||
defaultDeviceId,
|
||||
preKey?.preKeyId,
|
||||
|
|
@ -72,16 +70,16 @@ Future<Uint8List?> signalEncryptMessage(
|
|||
try {
|
||||
await sessionBuilder.processPreKeyBundle(preKeyBundle);
|
||||
} catch (e) {
|
||||
Log.error("could not process pre key bundle: $e");
|
||||
Log.error('could not process pre key bundle: $e');
|
||||
}
|
||||
} 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);
|
||||
|
||||
var b = BytesBuilder();
|
||||
final b = BytesBuilder();
|
||||
b.add(ciphertext.serialize());
|
||||
b.add(intToBytes(ciphertext.getType()));
|
||||
|
||||
|
|
@ -95,36 +93,34 @@ Future<Uint8List?> signalEncryptMessage(
|
|||
|
||||
Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
|
||||
try {
|
||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||
final signalStore = (await getSignalStore())!;
|
||||
|
||||
SessionCipher session = SessionCipher.fromStore(
|
||||
final session = SessionCipher.fromStore(
|
||||
signalStore, SignalProtocolAddress(source.toString(), defaultDeviceId));
|
||||
|
||||
List<Uint8List>? msgs = removeLastXBytes(msg, 4);
|
||||
final msgs = removeLastXBytes(msg, 4);
|
||||
if (msgs == null) {
|
||||
Log.error("Message requires at least 4 bytes.");
|
||||
Log.error('Message requires at least 4 bytes.');
|
||||
return null;
|
||||
}
|
||||
Uint8List body = msgs[0];
|
||||
int type = bytesToInt(msgs[1]);
|
||||
final body = msgs[0];
|
||||
final type = bytesToInt(msgs[1]);
|
||||
Uint8List plaintext;
|
||||
if (type == CiphertextMessage.prekeyType) {
|
||||
PreKeySignalMessage pre = PreKeySignalMessage(body);
|
||||
final pre = PreKeySignalMessage(body);
|
||||
plaintext = await session.decrypt(pre);
|
||||
} else if (type == CiphertextMessage.whisperType) {
|
||||
SignalMessage signalMsg = SignalMessage.fromSerialized(body);
|
||||
final signalMsg = SignalMessage.fromSerialized(body);
|
||||
plaintext = await session.decryptFromSignal(signalMsg);
|
||||
} else {
|
||||
Log.error("Type not known: $type");
|
||||
Log.error('Type not known: $type');
|
||||
return null;
|
||||
}
|
||||
return MessageJson.fromJson(
|
||||
jsonDecode(
|
||||
return MessageJson.fromJson(jsonDecode(
|
||||
utf8.decode(
|
||||
gzip.decode(plaintext),
|
||||
),
|
||||
),
|
||||
);
|
||||
) as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
Log.error(e.toString());
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.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/model/json/userdata.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/model/json/signal_identity.dart';
|
||||
import 'package:twonly/src/services/signal/consts.signal.dart';
|
||||
import 'package:twonly/src/services/signal/utils.signal.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.
|
||||
// It then checks if it should update a new session key
|
||||
Future signalHandleNewServerConnection() async {
|
||||
final UserData? user = await getUser();
|
||||
Future<void> signalHandleNewServerConnection() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
if (user.signalLastSignedPreKeyUpdated != null) {
|
||||
DateTime fortyEightHoursAgo = DateTime.now().subtract(Duration(hours: 48));
|
||||
bool isYoungerThan48Hours =
|
||||
final fortyEightHoursAgo =
|
||||
DateTime.now().subtract(const Duration(hours: 48));
|
||||
final isYoungerThan48Hours =
|
||||
(user.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo);
|
||||
if (isYoungerThan48Hours) {
|
||||
// The key does live for 48 hours then it expires and a new key is generated.
|
||||
return;
|
||||
}
|
||||
}
|
||||
SignedPreKeyRecord? signedPreKey = await _getNewSignalSignedPreKey();
|
||||
final signedPreKey = await _getNewSignalSignedPreKey();
|
||||
if (signedPreKey == null) {
|
||||
Log.error("could not generate a new signed pre key!");
|
||||
Log.error('could not generate a new signed pre key!');
|
||||
return;
|
||||
}
|
||||
await updateUserdata((user) {
|
||||
user.signalLastSignedPreKeyUpdated = DateTime.now();
|
||||
return user;
|
||||
});
|
||||
Result res = await apiService.updateSignedPreKey(
|
||||
final res = await apiService.updateSignedPreKey(
|
||||
signedPreKey.id,
|
||||
signedPreKey.getKeyPair().publicKey.serialize(),
|
||||
signedPreKey.signature,
|
||||
);
|
||||
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) {
|
||||
user.signalLastSignedPreKeyUpdated = null;
|
||||
return user;
|
||||
});
|
||||
} 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();
|
||||
if (user == null) return [];
|
||||
|
||||
int start = user.currentPreKeyIndexStart;
|
||||
final start = user.currentPreKeyIndexStart;
|
||||
await updateUserdata((user) {
|
||||
user.currentPreKeyIndexStart += 200;
|
||||
return user;
|
||||
|
|
@ -77,7 +77,7 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
|
|||
|
||||
Future<SignalIdentity?> getSignalIdentity() async {
|
||||
try {
|
||||
final storage = FlutterSecureStorage();
|
||||
const storage = FlutterSecureStorage();
|
||||
var signalIdentityJson =
|
||||
await storage.read(key: SecureStorageKeys.signalIdentity);
|
||||
if (signalIdentityJson == null) {
|
||||
|
|
@ -85,15 +85,15 @@ Future<SignalIdentity?> getSignalIdentity() async {
|
|||
}
|
||||
final decoded = jsonDecode(signalIdentityJson);
|
||||
signalIdentityJson = null;
|
||||
return SignalIdentity.fromJson(decoded);
|
||||
return SignalIdentity.fromJson(decoded as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
Log.error("could not load signal identity: $e");
|
||||
Log.error('could not load signal identity: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future createIfNotExistsSignalIdentity() async {
|
||||
final storage = FlutterSecureStorage();
|
||||
Future<void> createIfNotExistsSignalIdentity() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
final signalIdentity = await storage.read(
|
||||
key: SecureStorageKeys.signalIdentity,
|
||||
|
|
@ -106,7 +106,7 @@ Future createIfNotExistsSignalIdentity() async {
|
|||
final identityKeyPair = generateIdentityKeyPair();
|
||||
final registrationId = generateRegistrationId(true);
|
||||
|
||||
ConnectSignalProtocolStore signalStore =
|
||||
final signalStore =
|
||||
ConnectSignalProtocolStore(identityKeyPair, registrationId);
|
||||
|
||||
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
|
||||
|
|
@ -133,13 +133,13 @@ Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
|
|||
return null;
|
||||
}
|
||||
|
||||
int signedPreKeyId = user.currentSignedPreKeyIndexStart;
|
||||
final signedPreKeyId = user.currentSignedPreKeyIndexStart;
|
||||
await updateUserdata((user) {
|
||||
user.currentSignedPreKeyIndexStart += 1;
|
||||
return user;
|
||||
});
|
||||
|
||||
final SignedPreKeyRecord signedPreKey = generateSignedPreKey(
|
||||
final signedPreKey = generateSignedPreKey(
|
||||
identityKeyPair,
|
||||
signedPreKeyId,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Mutex requestNewKeys = Mutex();
|
|||
DateTime lastPreKeyRequest = 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
|
||||
.isAfter(DateTime.now().subtract(Duration(seconds: 60)))) {
|
||||
return;
|
||||
|
|
@ -59,7 +59,7 @@ Future<SignalContactPreKey?> getPreKeyByContactId(int contactId) async {
|
|||
return twonlyDB.signalDao.popPreKeyByContactId(contactId);
|
||||
}
|
||||
|
||||
Future requestNewSignedPreKeyForContact(int contactId) async {
|
||||
Future<void> requestNewSignedPreKeyForContact(int contactId) async {
|
||||
if (lastSignedPreKeyRequest
|
||||
.isAfter(DateTime.now().subtract(Duration(seconds: 60)))) {
|
||||
Log.info("last signed pre request was 60s before");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
if (signalStore == null) return;
|
||||
final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<void> createThumbnails(String directoryPath) async {
|
||||
final directory = Directory(directoryPath);
|
||||
final outputDirectory = await getTemporaryDirectory();
|
||||
|
||||
if (await directory.exists()) {
|
||||
final List<FileSystemEntity> files = directory.listSync();
|
||||
if (directory.existsSync()) {
|
||||
final files = directory.listSync();
|
||||
|
||||
for (var file in files) {
|
||||
for (final file in files) {
|
||||
if (file is File) {
|
||||
final String filePath = file.path;
|
||||
final String fileExtension = filePath.split('.').last.toLowerCase();
|
||||
final filePath = file.path;
|
||||
final fileExtension = filePath.split('.').last.toLowerCase();
|
||||
|
||||
if (['jpg', 'jpeg', 'png'].contains(fileExtension)) {
|
||||
// Create thumbnail for images
|
||||
|
|
@ -26,23 +27,18 @@ Future<void> createThumbnails(String directoryPath) async {
|
|||
final thumbnailFile =
|
||||
File('${outputDirectory.path}/${file.uri.pathSegments.last}');
|
||||
await thumbnailFile.writeAsBytes(thumbnail!.buffer.asUint8List());
|
||||
print('Thumbnail created for image: ${file.uri.pathSegments.last}');
|
||||
} else if (['mp4', 'mov', 'avi'].contains(fileExtension)) {
|
||||
// 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 {
|
||||
final String fileExtension = file.path.split('.').last.toLowerCase();
|
||||
if (fileExtension != "png") {
|
||||
Log.error("Could not create thumbnail for image. $fileExtension != .png");
|
||||
Future<void> createThumbnailsForImage(File file) async {
|
||||
final fileExtension = file.path.split('.').last.toLowerCase();
|
||||
if (fileExtension != 'png') {
|
||||
Log.error('Could not create thumbnail for image. $fileExtension != .png');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -56,45 +52,44 @@ Future createThumbnailsForImage(File file) async {
|
|||
);
|
||||
|
||||
if (imageBytesCompressed == null) {
|
||||
Log.error("Could not compress the image");
|
||||
Log.error('Could not compress the image');
|
||||
return;
|
||||
}
|
||||
|
||||
File thumbnailFile = getThumbnailPath(file);
|
||||
final thumbnailFile = getThumbnailPath(file);
|
||||
await thumbnailFile.writeAsBytes(imageBytesCompressed);
|
||||
} 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 {
|
||||
final String fileExtension = file.path.split('.').last.toLowerCase();
|
||||
if (fileExtension != "mp4") {
|
||||
Log.error("Could not create thumbnail for video. $fileExtension != .mp4");
|
||||
Future<void> createThumbnailsForVideo(File file) async {
|
||||
final fileExtension = file.path.split('.').last.toLowerCase();
|
||||
if (fileExtension != 'mp4') {
|
||||
Log.error('Could not create thumbnail for video. $fileExtension != .mp4');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await VideoThumbnail.thumbnailFile(
|
||||
video: file.path,
|
||||
imageFormat: ImageFormat.PNG,
|
||||
thumbnailPath: getThumbnailPath(file).path,
|
||||
maxWidth: 450,
|
||||
quality: 75,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error("Could not create the video thumbnail: $e");
|
||||
Log.error('Could not create the video thumbnail: $e');
|
||||
}
|
||||
}
|
||||
|
||||
File getThumbnailPath(File file) {
|
||||
String originalFileName = file.uri.pathSegments.last;
|
||||
String fileNameWithoutExtension = originalFileName.split('.').first;
|
||||
String fileExtension = originalFileName.split('.').last;
|
||||
if (fileExtension == "mp4") {
|
||||
fileExtension = "png";
|
||||
final originalFileName = file.uri.pathSegments.last;
|
||||
final fileNameWithoutExtension = originalFileName.split('.').first;
|
||||
var fileExtension = originalFileName.split('.').last;
|
||||
if (fileExtension == 'mp4') {
|
||||
fileExtension = 'png';
|
||||
}
|
||||
String newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension';
|
||||
final newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension';
|
||||
Directory(file.parent.path).createSync();
|
||||
return File(join(file.parent.path, newFileName));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/storage.dart';
|
||||
|
||||
Future enableTwonlySafe(String password) async {
|
||||
Future<void> enableTwonlySafe(String password) async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ Future enableTwonlySafe(String password) async {
|
|||
performTwonlySafeBackup(force: true);
|
||||
}
|
||||
|
||||
Future disableTwonlySafe() async {
|
||||
Future<void> disableTwonlySafe() async {
|
||||
final serverUrl = await getTwonlySafeBackupUrl();
|
||||
if (serverUrl != null) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import 'package:twonly/src/utils/log.dart';
|
|||
import 'package:twonly/src/utils/storage.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();
|
||||
|
||||
if (user == null || user.twonlySafeBackup == null || user.isDemoUser) {
|
||||
|
|
@ -27,38 +27,39 @@ Future performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
if (user.twonlySafeBackup!.backupUploadState ==
|
||||
LastBackupUploadState.pending) {
|
||||
Log.warn("Backup upload is already pending.");
|
||||
Log.warn('Backup upload is already pending.');
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime? lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
|
||||
final DateTime? lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
|
||||
if (!force && lastUpdateTime != null) {
|
||||
if (lastUpdateTime.isAfter(DateTime.now().subtract(Duration(days: 1)))) {
|
||||
if (lastUpdateTime
|
||||
.isAfter(DateTime.now().subtract(const Duration(days: 1)))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.info("Starting new twonly Safe-Backup!");
|
||||
Log.info('Starting new twonly Safe-Backup!');
|
||||
|
||||
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);
|
||||
|
||||
final backupDatabaseFile =
|
||||
File(join(backupDir.path, "twonly_database.backup.sqlite"));
|
||||
File(join(backupDir.path, 'twonly_database.backup.sqlite'));
|
||||
|
||||
final backupDatabaseFileCleaned =
|
||||
File(join(backupDir.path, "twonly_database.backup.cleaned.sqlite"));
|
||||
File(join(backupDir.path, 'twonly_database.backup.cleaned.sqlite'));
|
||||
|
||||
// copy database
|
||||
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
|
||||
final originalDatabase = File(join(baseDir, 'twonly_database.sqlite'));
|
||||
await originalDatabase.copy(backupDatabaseFile.path);
|
||||
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
final backupDB = TwonlyDatabase(
|
||||
driftDatabase(
|
||||
name: "twonly_database.backup",
|
||||
name: 'twonly_database.backup',
|
||||
native: DriftNativeOptions(
|
||||
databaseDirectory: () async {
|
||||
return backupDir;
|
||||
|
|
@ -74,24 +75,26 @@ Future performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
await backupDB.printTableSizes();
|
||||
|
||||
backupDB.close();
|
||||
await backupDB.close();
|
||||
|
||||
var secureStorageBackup = {};
|
||||
final storage = FlutterSecureStorage();
|
||||
// ignore: inference_failure_on_collection_literal
|
||||
final secureStorageBackup = {};
|
||||
const storage = FlutterSecureStorage();
|
||||
secureStorageBackup[SecureStorageKeys.signalIdentity] =
|
||||
await storage.read(key: SecureStorageKeys.signalIdentity);
|
||||
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
|
||||
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
|
||||
|
||||
var userBackup = await getUser();
|
||||
final userBackup = await getUser();
|
||||
if (userBackup == null) return;
|
||||
// FILTER settings which should not be in the backup
|
||||
userBackup.twonlySafeBackup = null;
|
||||
userBackup.lastImageSend = null;
|
||||
userBackup.todaysImageCounter = null;
|
||||
userBackup.lastPlanBallance = "";
|
||||
userBackup.additionalUserInvites = "";
|
||||
userBackup.signalLastSignedPreKeyUpdated = null;
|
||||
userBackup
|
||||
..twonlySafeBackup = null
|
||||
..lastImageSend = null
|
||||
..todaysImageCounter = null
|
||||
..lastPlanBallance = ''
|
||||
..additionalUserInvites = ''
|
||||
..signalLastSignedPreKeyUpdated = null;
|
||||
|
||||
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
|
||||
|
||||
|
|
@ -101,8 +104,8 @@ Future performTwonlySafeBackup({bool force = false}) async {
|
|||
await backupDatabaseFile.delete();
|
||||
await backupDatabaseFileCleaned.delete();
|
||||
|
||||
Log.info("twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}");
|
||||
Log.info("secureStorageLength = ${jsonEncode(secureStorageBackup).length}");
|
||||
Log.info('twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}');
|
||||
Log.info('secureStorageLength = ${jsonEncode(secureStorageBackup).length}');
|
||||
|
||||
final backupProto = TwonlySafeBackupContent(
|
||||
secureStorageJson: jsonEncode(secureStorageBackup),
|
||||
|
|
@ -115,7 +118,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
if (user.twonlySafeBackup!.lastBackupDone == null ||
|
||||
user.twonlySafeBackup!.lastBackupDone!
|
||||
.isAfter(DateTime.now().subtract(Duration(days: 90)))) {
|
||||
.isAfter(DateTime.now().subtract(const Duration(days: 90)))) {
|
||||
force = true;
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +127,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
if (lastHash != null && !force) {
|
||||
if (backupHash == lastHash) {
|
||||
Log.info("Since last backup nothing has changed.");
|
||||
Log.info('Since last backup nothing has changed.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -144,25 +147,25 @@ Future performTwonlySafeBackup({bool force = false}) async {
|
|||
nonce: nonce,
|
||||
);
|
||||
|
||||
final encryptedBackupBytes = (TwonlySafeBackupEncrypted(
|
||||
final encryptedBackupBytes = TwonlySafeBackupEncrypted(
|
||||
mac: secretBox.mac.bytes,
|
||||
nonce: nonce,
|
||||
cipherText: secretBox.cipherText,
|
||||
)).writeToBuffer();
|
||||
).writeToBuffer();
|
||||
|
||||
Log.info("Backup files created.");
|
||||
Log.info('Backup files created.');
|
||||
|
||||
var encryptedBackupBytesFile =
|
||||
File(join(backupDir.path, "twonly_safe.backup"));
|
||||
final encryptedBackupBytesFile =
|
||||
File(join(backupDir.path, 'twonly_safe.backup'));
|
||||
|
||||
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
|
||||
|
||||
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 (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) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
||||
return user;
|
||||
|
|
@ -172,20 +175,20 @@ Future performTwonlySafeBackup({bool force = false}) async {
|
|||
}
|
||||
|
||||
final task = UploadTask.fromFile(
|
||||
taskId: "backup",
|
||||
taskId: 'backup',
|
||||
file: encryptedBackupBytesFile,
|
||||
httpRequestMethod: "PUT",
|
||||
httpRequestMethod: 'PUT',
|
||||
url: (await getTwonlySafeBackupUrl())!,
|
||||
// requiresWiFi: true,
|
||||
priority: 5,
|
||||
post: 'binary',
|
||||
retries: 2,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
);
|
||||
if (await FileDownloader().enqueue(task)) {
|
||||
Log.info("Starting upload from twonly Safe backup.");
|
||||
Log.info('Starting upload from twonly Safe backup.');
|
||||
await updateUserdata((user) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
|
||||
user.twonlySafeBackup!.lastBackupDone = DateTime.now();
|
||||
|
|
@ -194,15 +197,15 @@ Future performTwonlySafeBackup({bool force = false}) async {
|
|||
});
|
||||
gUpdateBackupView();
|
||||
} 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 ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
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) {
|
||||
if (user.twonlySafeBackup != null) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
||||
|
|
@ -211,7 +214,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
|
|||
});
|
||||
} else if (update.status == TaskStatus.complete) {
|
||||
Log.error(
|
||||
"twonly Safe uploaded with status code ${update.responseStatusCode}");
|
||||
'twonly Safe uploaded with status code ${update.responseStatusCode}');
|
||||
await updateUserdata((user) {
|
||||
if (user.twonlySafeBackup != null) {
|
||||
user.twonlySafeBackup!.backupUploadState =
|
||||
|
|
@ -220,7 +223,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
|
|||
return user;
|
||||
});
|
||||
} else {
|
||||
Log.info("Backup is in state: ${update.status}");
|
||||
Log.info('Backup is in state: ${update.status}');
|
||||
return;
|
||||
}
|
||||
gUpdateBackupView();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: avoid_dynamic_calls
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
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/utils/log.dart';
|
||||
|
||||
Future recoverTwonlySafe(
|
||||
Future<void> recoverTwonlySafe(
|
||||
String username,
|
||||
String password,
|
||||
BackupServer? server,
|
||||
) async {
|
||||
final (backupId, encryptionKey) = await getMasterKey(password, username);
|
||||
|
||||
String? backupServerUrl =
|
||||
final backupServerUrl =
|
||||
await getTwonlySafeBackupUrlFromServer(backupId, server);
|
||||
|
||||
if (backupServerUrl == null) {
|
||||
Log.error("Could not create backup url");
|
||||
throw Exception("Could not create backup server url");
|
||||
Log.error('Could not create backup url');
|
||||
throw Exception('Could not create backup server url');
|
||||
}
|
||||
|
||||
late Uint8List backupData;
|
||||
|
|
@ -39,7 +41,7 @@ Future recoverTwonlySafe(
|
|||
});
|
||||
} catch (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) {
|
||||
|
|
@ -55,19 +57,18 @@ Future recoverTwonlySafe(
|
|||
throw Exception('Unexpected error: ${response.statusCode}');
|
||||
}
|
||||
|
||||
return await handleBackupData(encryptionKey, backupData);
|
||||
return handleBackupData(encryptionKey, backupData);
|
||||
}
|
||||
|
||||
Future handleBackupData(
|
||||
Future<void> handleBackupData(
|
||||
Uint8List encryptionKey,
|
||||
Uint8List backupData,
|
||||
) async {
|
||||
TwonlySafeBackupEncrypted encryptedBackup =
|
||||
TwonlySafeBackupEncrypted.fromBuffer(
|
||||
final encryptedBackup = TwonlySafeBackupEncrypted.fromBuffer(
|
||||
backupData,
|
||||
);
|
||||
|
||||
SecretBox secretBox = SecretBox(
|
||||
final secretBox = SecretBox(
|
||||
encryptedBackup.cipherText,
|
||||
nonce: encryptedBackup.nonce,
|
||||
mac: Mac(encryptedBackup.mac),
|
||||
|
|
@ -80,12 +81,12 @@ Future handleBackupData(
|
|||
|
||||
final plaintextBytes = gzip.decode(compressedBytes);
|
||||
|
||||
TwonlySafeBackupContent backupContent = TwonlySafeBackupContent.fromBuffer(
|
||||
final backupContent = TwonlySafeBackupContent.fromBuffer(
|
||||
plaintextBytes,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
/// When restoring the last message ID must be increased otherwise
|
||||
|
|
@ -106,33 +107,33 @@ Future handleBackupData(
|
|||
|
||||
if (randomUserId != null) {
|
||||
// for each day add 400 message ids
|
||||
var dummyMessagesCounter = (lastMessageSend + 1) * 400;
|
||||
final dummyMessagesCounter = (lastMessageSend + 1) * 400;
|
||||
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++) {
|
||||
await database.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
contactId: Value(randomUserId),
|
||||
kind: Value(MessageKind.ack),
|
||||
acknowledgeByServer: Value(true),
|
||||
errorWhileSending: Value(true),
|
||||
kind: const Value(MessageKind.ack),
|
||||
acknowledgeByServer: const Value(true),
|
||||
errorWhileSending: const Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
await database.messagesDao.deleteAllMessagesByContactId(randomUserId);
|
||||
}
|
||||
|
||||
final storage = FlutterSecureStorage();
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
final secureStorage = jsonDecode(backupContent.secureStorageJson);
|
||||
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.signalIdentity,
|
||||
value: secureStorage[SecureStorageKeys.signalIdentity]);
|
||||
value: secureStorage[SecureStorageKeys.signalIdentity] as String);
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.signalSignedPreKey,
|
||||
value: secureStorage[SecureStorageKeys.signalSignedPreKey]);
|
||||
value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String);
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: secureStorage[SecureStorageKeys.userData]);
|
||||
value: secureStorage[SecureStorageKeys.userData] as String);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ class KeyValueStore {
|
|||
final file = File(filePath);
|
||||
|
||||
// Check if the file exists
|
||||
if (await file.exists()) {
|
||||
if (file.existsSync()) {
|
||||
final contents = await file.readAsString();
|
||||
return jsonDecode(contents);
|
||||
return jsonDecode(contents) as Map<String, dynamic>;
|
||||
} else {
|
||||
return null; // File does not exist
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -12,9 +13,9 @@ import 'package:provider/provider.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.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/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/utils/log.dart';
|
||||
|
||||
|
|
@ -51,10 +52,10 @@ Future<String?> saveVideoToGallery(String videoPath) async {
|
|||
}
|
||||
|
||||
Uint8List getRandomUint8List(int length) {
|
||||
final Random random = Random.secure();
|
||||
final Uint8List randomBytes = Uint8List(length);
|
||||
final random = Random.secure();
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ Uint8List getRandomUint8List(int length) {
|
|||
}
|
||||
|
||||
String errorCodeToText(BuildContext context, ErrorCode code) {
|
||||
// ignore: exhaustive_cases
|
||||
switch (code) {
|
||||
case ErrorCode.InternalError:
|
||||
return context.lang.errorInternalError;
|
||||
|
|
@ -81,22 +83,21 @@ String errorCodeToText(BuildContext context, ErrorCode code) {
|
|||
return context.lang.errorVoucherInvalid;
|
||||
case ErrorCode.PlanUpgradeNotYearly:
|
||||
return context.lang.errorPlanUpgradeNotYearly;
|
||||
default:
|
||||
return code.toString(); // Fallback for unrecognized keys
|
||||
}
|
||||
return code.toString(); // Fallback for unrecognized keys
|
||||
}
|
||||
|
||||
String formatDuration(int seconds) {
|
||||
if (seconds < 60) {
|
||||
return '$seconds Sec.';
|
||||
} else if (seconds < 3600) {
|
||||
int minutes = seconds ~/ 60;
|
||||
final minutes = seconds ~/ 60;
|
||||
return '$minutes Min.';
|
||||
} else if (seconds < 86400) {
|
||||
int hours = seconds ~/ 3600;
|
||||
final hours = seconds ~/ 3600;
|
||||
return '$hours Hrs.'; // Assuming "Stu." is for hours
|
||||
} else {
|
||||
int days = seconds ~/ 86400;
|
||||
final days = seconds ~/ 86400;
|
||||
return '$days Days';
|
||||
}
|
||||
}
|
||||
|
|
@ -107,20 +108,19 @@ InputDecoration getInputDecoration(BuildContext context, String hintText) {
|
|||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
borderSide: BorderSide(color: primaryColor, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
borderSide: BorderSide(color: primaryColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.outline, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
|
||||
var result = await FlutterImageCompress.compressWithList(
|
||||
final result = await FlutterImageCompress.compressWithList(
|
||||
imageBytes,
|
||||
quality: 90,
|
||||
);
|
||||
|
|
@ -130,8 +130,8 @@ Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
|
|||
Future<bool> authenticateUser(String localizedReason,
|
||||
{bool force = true}) async {
|
||||
try {
|
||||
final LocalAuthentication auth = LocalAuthentication();
|
||||
bool didAuthenticate = await auth.authenticate(
|
||||
final auth = LocalAuthentication();
|
||||
final didAuthenticate = await auth.authenticate(
|
||||
localizedReason: localizedReason,
|
||||
options: const AuthenticationOptions(useErrorDialogs: false));
|
||||
if (didAuthenticate) {
|
||||
|
|
@ -147,31 +147,30 @@ Future<bool> authenticateUser(String localizedReason,
|
|||
}
|
||||
|
||||
Uint8List intToBytes(int value) {
|
||||
final byteData = ByteData(4);
|
||||
byteData.setInt32(0, value, Endian.big);
|
||||
final byteData = ByteData(4)..setInt32(0, value);
|
||||
return byteData.buffer.asUint8List();
|
||||
}
|
||||
|
||||
int bytesToInt(Uint8List bytes) {
|
||||
final byteData = ByteData.sublistView(bytes);
|
||||
return byteData.getInt32(0, Endian.big);
|
||||
return byteData.getInt32(0);
|
||||
}
|
||||
|
||||
List<Uint8List>? removeLastXBytes(Uint8List original, int count) {
|
||||
if (original.length < count) {
|
||||
return null;
|
||||
}
|
||||
final newList = Uint8List(original.length - count);
|
||||
newList.setAll(0, original.sublist(0, original.length - count));
|
||||
final newList = Uint8List(original.length - count)
|
||||
..setAll(0, original.sublist(0, original.length - count));
|
||||
|
||||
final lastXBytes = original.sublist(original.length - count);
|
||||
return [newList, lastXBytes];
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return selectedTheme == ThemeMode.dark ||
|
||||
|
|
@ -188,20 +187,20 @@ bool isToday(DateTime lastImageSend) {
|
|||
InputDecoration inputTextMessageDeco(BuildContext context) {
|
||||
return InputDecoration(
|
||||
hintText: context.lang.chatListDetailInput,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
borderSide: BorderSide(color: Colors.grey, width: 2.0),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide: const BorderSide(color: Colors.grey, width: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -213,8 +212,8 @@ String truncateString(String input, {int maxLength = 20}) {
|
|||
return input;
|
||||
}
|
||||
|
||||
Future insertDemoContacts() async {
|
||||
List<String> commonUsernames = [
|
||||
Future<void> insertDemoContacts() async {
|
||||
final commonUsernames = <String>[
|
||||
'James',
|
||||
'Mary',
|
||||
'John',
|
||||
|
|
@ -236,7 +235,7 @@ Future insertDemoContacts() async {
|
|||
'Thomas',
|
||||
'Karen',
|
||||
];
|
||||
final List<Map<String, dynamic>> contactConfigs = [
|
||||
final contactConfigs = <Map<String, dynamic>>[
|
||||
{'count': 3, 'requested': true},
|
||||
{'count': 4, 'requested': false, 'accepted': true},
|
||||
{'count': 1, 'accepted': true, 'blocked': true},
|
||||
|
|
@ -245,43 +244,44 @@ Future insertDemoContacts() async {
|
|||
{'count': 1, 'requested': false},
|
||||
];
|
||||
|
||||
int counter = 0;
|
||||
var counter = 0;
|
||||
|
||||
for (var config in contactConfigs) {
|
||||
for (int i = 0; i < config['count']; i++) {
|
||||
for (final config in contactConfigs) {
|
||||
for (var i = 0; i < (config['count'] as int); i++) {
|
||||
if (counter >= commonUsernames.length) {
|
||||
break;
|
||||
}
|
||||
String username = commonUsernames[counter];
|
||||
int userId = Random().nextInt(1000000);
|
||||
final username = commonUsernames[counter];
|
||||
final userId = Random().nextInt(1000000);
|
||||
await twonlyDB.contactsDao.insertContact(
|
||||
ContactsCompanion(
|
||||
username: Value(username),
|
||||
userId: Value(userId),
|
||||
requested: Value(config['requested'] ?? false),
|
||||
accepted: Value(config['accepted'] ?? false),
|
||||
blocked: Value(config['blocked'] ?? false),
|
||||
archived: Value(config['archived'] ?? false),
|
||||
pinned: Value(config['pinned'] ?? false),
|
||||
requested: Value(config['requested'] as bool? ?? false),
|
||||
accepted: Value(config['accepted'] as bool? ?? false),
|
||||
blocked: Value(config['blocked'] as bool? ?? false),
|
||||
archived: Value(config['archived'] as bool? ?? 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++) {
|
||||
int chatId = Random().nextInt(chatMessages.length);
|
||||
final chatId = Random().nextInt(chatMessages.length);
|
||||
await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
contactId: Value(userId),
|
||||
kind: Value(MessageKind.textMessage),
|
||||
sendAt: Value(chatMessages[chatId][1]),
|
||||
acknowledgeByServer: Value(true),
|
||||
acknowledgeByUser: Value(true),
|
||||
kind: const Value(MessageKind.textMessage),
|
||||
sendAt: Value(chatMessages[chatId][1] as DateTime),
|
||||
acknowledgeByServer: const Value(true),
|
||||
acknowledgeByUser: const Value(true),
|
||||
messageOtherId:
|
||||
Value(Random().nextBool() ? Random().nextInt(10000) : null),
|
||||
// responseToOtherMessageId: Value(content.responseToMessageId),
|
||||
// responseToMessageId: Value(content.responseToOtherMessageId),
|
||||
downloadState: Value(DownloadState.downloaded),
|
||||
downloadState: const Value(DownloadState.downloaded),
|
||||
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();
|
||||
}
|
||||
|
||||
List<List<dynamic>> chatMessages = [
|
||||
[
|
||||
"Lorem ipsum dolor sit amet.",
|
||||
DateTime.now().subtract(Duration(minutes: 20))
|
||||
'Lorem ipsum dolor sit amet.',
|
||||
DateTime.now().subtract(const Duration(minutes: 20))
|
||||
],
|
||||
[
|
||||
"Consectetur adipiscing elit.",
|
||||
DateTime.now().subtract(Duration(minutes: 19))
|
||||
'Consectetur adipiscing elit.',
|
||||
DateTime.now().subtract(const Duration(minutes: 19))
|
||||
],
|
||||
[
|
||||
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
DateTime.now().subtract(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))
|
||||
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
DateTime.now().subtract(const Duration(minutes: 18))
|
||||
],
|
||||
[
|
||||
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
||||
DateTime.now().subtract(Duration(minutes: 15))
|
||||
'Ut enim ad minim veniam.',
|
||||
DateTime.now().subtract(const Duration(minutes: 17))
|
||||
],
|
||||
[
|
||||
"Excepteur sint occaecat cupidatat non proident.",
|
||||
DateTime.now().subtract(Duration(minutes: 14))
|
||||
'Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
|
||||
DateTime.now().subtract(const Duration(minutes: 16))
|
||||
],
|
||||
[
|
||||
"Sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
DateTime.now().subtract(Duration(minutes: 13))
|
||||
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
|
||||
DateTime.now().subtract(const Duration(minutes: 15))
|
||||
],
|
||||
[
|
||||
"Curabitur pretium tincidunt lacus.",
|
||||
DateTime.now().subtract(Duration(minutes: 12))
|
||||
],
|
||||
["Nulla facilisi.", DateTime.now().subtract(Duration(minutes: 11))],
|
||||
[
|
||||
"Aenean lacinia bibendum nulla sed consectetur.",
|
||||
DateTime.now().subtract(Duration(minutes: 10))
|
||||
'Excepteur sint occaecat cupidatat non proident.',
|
||||
DateTime.now().subtract(const Duration(minutes: 14))
|
||||
],
|
||||
[
|
||||
"Sed posuere consectetur est at lobortis.",
|
||||
DateTime.now().subtract(Duration(minutes: 9))
|
||||
'Sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||
DateTime.now().subtract(const Duration(minutes: 13))
|
||||
],
|
||||
[
|
||||
"Vestibulum id ligula porta felis euismod semper.",
|
||||
DateTime.now().subtract(Duration(minutes: 8))
|
||||
'Curabitur pretium tincidunt lacus.',
|
||||
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.",
|
||||
DateTime.now().subtract(Duration(minutes: 7))
|
||||
'Sed posuere consectetur est at lobortis.',
|
||||
DateTime.now().subtract(const Duration(minutes: 9))
|
||||
],
|
||||
[
|
||||
"Morbi leo risus, porta ac consectetur ac, vestibulum at eros.",
|
||||
DateTime.now().subtract(Duration(minutes: 6))
|
||||
'Vestibulum id ligula porta felis euismod semper.',
|
||||
DateTime.now().subtract(const Duration(minutes: 8))
|
||||
],
|
||||
[
|
||||
"Praesent commodo cursus magna, vel scelerisque nisl consectetur et.",
|
||||
DateTime.now().subtract(Duration(minutes: 5))
|
||||
'Cras justo odio, dapibus ac facilisis in, egestas eget quam.',
|
||||
DateTime.now().subtract(const Duration(minutes: 7))
|
||||
],
|
||||
[
|
||||
"Donec ullamcorper nulla non metus auctor fringilla.",
|
||||
DateTime.now().subtract(Duration(minutes: 4))
|
||||
'Morbi leo risus, porta ac consectetur ac, vestibulum at eros.',
|
||||
DateTime.now().subtract(const Duration(minutes: 6))
|
||||
],
|
||||
[
|
||||
"Etiam porta sem malesuada magna mollis euismod.",
|
||||
DateTime.now().subtract(Duration(minutes: 3))
|
||||
'Praesent commodo cursus magna, vel scelerisque nisl consectetur et.',
|
||||
DateTime.now().subtract(const Duration(minutes: 5))
|
||||
],
|
||||
[
|
||||
"Aenean lacinia bibendum nulla sed consectetur.",
|
||||
DateTime.now().subtract(Duration(minutes: 2))
|
||||
'Donec ullamcorper nulla non metus auctor fringilla.',
|
||||
DateTime.now().subtract(const Duration(minutes: 4))
|
||||
],
|
||||
[
|
||||
"Nullam quis risus eget urna mollis ornare vel eu leo.",
|
||||
DateTime.now().subtract(Duration(minutes: 1))
|
||||
'Etiam porta sem malesuada magna mollis euismod.',
|
||||
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) {
|
||||
if (dateTime == null) {
|
||||
return "Never";
|
||||
return 'Never';
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
|
@ -390,32 +393,34 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
|
|||
if (difference.inDays == 0) {
|
||||
return time;
|
||||
} else {
|
||||
return "$time $date";
|
||||
return '$time $date';
|
||||
}
|
||||
}
|
||||
|
||||
String formatBytes(int bytes, {int decimalPlaces = 2}) {
|
||||
if (bytes <= 0) return "0 Bytes";
|
||||
const List<String> units = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
final int unitIndex = (log(bytes) / log(1000)).floor();
|
||||
final double formattedSize = bytes / pow(1000, unitIndex);
|
||||
return "${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}";
|
||||
if (bytes <= 0) return '0 Bytes';
|
||||
const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
final unitIndex = (log(bytes) / log(1000)).floor();
|
||||
final formattedSize = bytes / pow(1000, unitIndex);
|
||||
return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}';
|
||||
}
|
||||
|
||||
String getMessageText(Message message) {
|
||||
try {
|
||||
if (message.contentJson == null) return "";
|
||||
return TextMessageContent.fromJson(jsonDecode(message.contentJson!)).text;
|
||||
if (message.contentJson == null) return '';
|
||||
return TextMessageContent.fromJson(jsonDecode(message.contentJson!) as Map)
|
||||
.text;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
MediaMessageContent? getMediaContent(Message message) {
|
||||
try {
|
||||
if (message.contentJson == null) return null;
|
||||
return MediaMessageContent.fromJson(jsonDecode(message.contentJson!));
|
||||
return MediaMessageContent.fromJson(
|
||||
jsonDecode(message.contentJson!) as Map);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import 'package:twonly/src/providers/connection.provider.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<bool> isUserCreated() async {
|
||||
UserData? user = await getUser();
|
||||
final user = await getUser();
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@ Future<bool> isUserCreated() async {
|
|||
}
|
||||
|
||||
Future<UserData?> getUser() async {
|
||||
String? userJson =
|
||||
await FlutterSecureStorage().read(key: SecureStorageKeys.userData);
|
||||
final userJson =
|
||||
await const FlutterSecureStorage().read(key: SecureStorageKeys.userData);
|
||||
if (userJson == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -28,12 +28,12 @@ Future<UserData?> getUser() async {
|
|||
final user = UserData.fromJson(userMap);
|
||||
return user;
|
||||
} catch (e) {
|
||||
Log.error("Error getting user: $e");
|
||||
Log.error('Error getting user: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future updateUsersPlan(BuildContext context, String planId) async {
|
||||
Future<void> updateUsersPlan(BuildContext context, String planId) async {
|
||||
context.read<CustomChangeProvider>().plan = planId;
|
||||
|
||||
await updateUserdata((user) {
|
||||
|
|
@ -42,17 +42,18 @@ Future updateUsersPlan(BuildContext context, String planId) async {
|
|||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
context.read<CustomChangeProvider>().updatePlan(planId);
|
||||
await context.read<CustomChangeProvider>().updatePlan(planId);
|
||||
}
|
||||
|
||||
Mutex updateProtection = Mutex();
|
||||
|
||||
Future<UserData?> updateUserdata(Function(UserData userData) updateUser) async {
|
||||
return await updateProtection.protect<UserData?>(() async {
|
||||
Future<UserData?> updateUserdata(
|
||||
UserData Function(UserData userData) updateUser) async {
|
||||
return updateProtection.protect<UserData?>(() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return null;
|
||||
UserData updated = updateUser(user);
|
||||
FlutterSecureStorage()
|
||||
final updated = updateUser(user);
|
||||
await const FlutterSecureStorage()
|
||||
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated));
|
||||
return user;
|
||||
});
|
||||
|
|
@ -63,6 +64,6 @@ Future<bool> deleteLocalUserData() async {
|
|||
if (appDir.existsSync()) {
|
||||
appDir.deleteSync(recursive: true);
|
||||
}
|
||||
await FlutterSecureStorage().deleteAll();
|
||||
await const FlutterSecureStorage().deleteAll();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
initAsync();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
Future<void> initAsync() async {
|
||||
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
|
||||
if (_isDisposed) return;
|
||||
setState(() {});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.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:screenshot/screenshot.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/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/video_recording_time.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/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/components/media_view_sizing.dart';
|
||||
import 'package:twonly/src/views/home.view.dart';
|
||||
|
||||
int maxVideoRecordingTime = 15;
|
||||
|
|
@ -43,7 +44,7 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
|
|||
details.scaleFactor = 1;
|
||||
}
|
||||
|
||||
CameraController cameraController = CameraController(
|
||||
final cameraController = CameraController(
|
||||
gCameras[sCameraId],
|
||||
ResolutionPreset.high,
|
||||
enableAudio: enableAudio,
|
||||
|
|
@ -52,7 +53,7 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
|
|||
await cameraController.initialize().then((_) async {
|
||||
await cameraController.setZoomLevel(details.scaleFactor);
|
||||
await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp);
|
||||
cameraController
|
||||
await cameraController
|
||||
.setFlashMode(details.isFlashOn ? FlashMode.always : FlashMode.off);
|
||||
await cameraController
|
||||
.getMaxZoomLevel()
|
||||
|
|
@ -60,9 +61,10 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
|
|||
await cameraController
|
||||
.getMinZoomLevel()
|
||||
.then((double value) => details.minAvailableZoom = value);
|
||||
details.isZoomAble = details.maxAvailableZoom != details.minAvailableZoom;
|
||||
details.cameraLoaded = true;
|
||||
details.cameraId = sCameraId;
|
||||
details
|
||||
..isZoomAble = details.maxAvailableZoom != details.minAvailableZoom
|
||||
..cameraLoaded = true
|
||||
..cameraId = sCameraId;
|
||||
}).catchError((Object e) {
|
||||
Log.error("$e");
|
||||
});
|
||||
|
|
@ -81,13 +83,13 @@ class SelectedCameraDetails {
|
|||
|
||||
class CameraPreviewControllerView extends StatefulWidget {
|
||||
const CameraPreviewControllerView({
|
||||
super.key,
|
||||
required this.selectCamera,
|
||||
required this.isHomeView,
|
||||
super.key,
|
||||
this.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;
|
||||
|
||||
@override
|
||||
|
|
@ -123,13 +125,13 @@ class _CameraPreviewControllerView extends State<CameraPreviewControllerView> {
|
|||
|
||||
class CameraPreviewView extends StatefulWidget {
|
||||
const CameraPreviewView(
|
||||
{super.key,
|
||||
this.sendTo,
|
||||
required this.selectCamera,
|
||||
required this.isHomeView});
|
||||
{required this.selectCamera,
|
||||
required this.isHomeView,
|
||||
super.key,
|
||||
this.sendTo});
|
||||
final Contact? sendTo;
|
||||
final bool isHomeView;
|
||||
final Function(int sCameraId, bool init, bool enableAudio) selectCamera;
|
||||
final void Function(int sCameraId, bool init, bool enableAudio) selectCamera;
|
||||
|
||||
@override
|
||||
State<CameraPreviewView> createState() => _CameraPreviewViewState();
|
||||
|
|
@ -171,7 +173,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
? HomeViewState.screenshotController
|
||||
: CameraSendToViewState.screenshotController;
|
||||
|
||||
void initAsync() async {
|
||||
Future<void> initAsync() async {
|
||||
hasAudioPermission = await Permission.microphone.isGranted;
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
|
|
@ -183,12 +185,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future requestMicrophonePermission() async {
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Future<void> requestMicrophonePermission() async {
|
||||
final statuses = await [
|
||||
Permission.microphone,
|
||||
].request();
|
||||
if (statuses[Permission.microphone]!.isPermanentlyDenied) {
|
||||
openAppSettings();
|
||||
await openAppSettings();
|
||||
} else {
|
||||
hasAudioPermission = await Permission.microphone.isGranted;
|
||||
setState(() {});
|
||||
|
|
@ -221,7 +223,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
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;
|
||||
late Future<Uint8List?> imageBytes;
|
||||
|
||||
|
|
@ -242,16 +244,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
showSelfieFlash = true;
|
||||
});
|
||||
} 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();
|
||||
if (!mounted) return;
|
||||
|
||||
cameraController?.setFlashMode(
|
||||
await cameraController?.setFlashMode(
|
||||
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off);
|
||||
if (!mounted) return;
|
||||
|
||||
imageBytes = screenshotController.capture(
|
||||
pixelRatio: MediaQuery.of(context).devicePixelRatio);
|
||||
|
||||
|
|
@ -263,7 +267,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
Future<bool> pushMediaEditor(
|
||||
Future<Uint8List?>? imageBytes, File? videoFilePath,
|
||||
{bool sharedFromGallery = false}) async {
|
||||
bool? shouldReturn = await Navigator.push(
|
||||
final shouldReturn = await Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
|
|
@ -281,7 +285,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
),
|
||||
);
|
||||
) as bool?;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
sharePreviewIsShown = false;
|
||||
|
|
@ -306,15 +310,16 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
bool get isFront =>
|
||||
cameraController?.description.lensDirection == CameraLensDirection.front;
|
||||
|
||||
Future onPanUpdate(details) async {
|
||||
if (isFront) {
|
||||
Future<void> onPanUpdate(dynamic details) async {
|
||||
if (isFront || details == null) {
|
||||
return;
|
||||
}
|
||||
if (cameraController == null) return;
|
||||
if (!cameraController!.value.isInitialized) return;
|
||||
|
||||
selectedCameraDetails.scaleFactor =
|
||||
(baseScaleFactor + (basePanY - details.localPosition.dy) / 30)
|
||||
// ignore: avoid_dynamic_calls
|
||||
(baseScaleFactor + (basePanY - (details.localPosition.dy as int)) / 30)
|
||||
.clamp(1, selectedCameraDetails.maxAvailableZoom);
|
||||
|
||||
await cameraController!.setZoomLevel(selectedCameraDetails.scaleFactor);
|
||||
|
|
@ -323,7 +328,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future pickImageFromGallery() async {
|
||||
Future<void> pickImageFromGallery() async {
|
||||
setState(() {
|
||||
galleryLoadedImageIsShown = true;
|
||||
sharePreviewIsShown = true;
|
||||
|
|
@ -332,7 +337,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||
|
||||
if (pickedFile != null) {
|
||||
File imageFile = File(pickedFile.path);
|
||||
final imageFile = File(pickedFile.path);
|
||||
await pushMediaEditor(
|
||||
imageFile.readAsBytes(),
|
||||
null,
|
||||
|
|
@ -345,12 +350,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
});
|
||||
}
|
||||
|
||||
Future startVideoRecording() async {
|
||||
Future<void> startVideoRecording() async {
|
||||
if (cameraController != null && cameraController!.value.isRecordingVideo) {
|
||||
return;
|
||||
}
|
||||
if (hasAudioPermission && videoWithAudio) {
|
||||
await widget.selectCamera(
|
||||
widget.selectCamera(
|
||||
selectedCameraDetails.cameraId,
|
||||
false,
|
||||
await Permission.microphone.isGranted && videoWithAudio,
|
||||
|
|
@ -363,7 +368,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
|
||||
try {
|
||||
await cameraController?.startVideoRecording();
|
||||
videoRecordingTimer = Timer.periodic(Duration(milliseconds: 15), (timer) {
|
||||
videoRecordingTimer =
|
||||
Timer.periodic(const Duration(milliseconds: 15), (timer) {
|
||||
setState(() {
|
||||
currentTime = DateTime.now();
|
||||
});
|
||||
|
|
@ -388,7 +394,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future stopVideoRecording() async {
|
||||
Future<void> stopVideoRecording() async {
|
||||
if (videoRecordingTimer != null) {
|
||||
videoRecordingTimer?.cancel();
|
||||
videoRecordingTimer = null;
|
||||
|
|
@ -404,7 +410,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
sharePreviewIsShown = true;
|
||||
});
|
||||
File? videoPathFile;
|
||||
XFile? videoPath = await cameraController?.stopVideoRecording();
|
||||
final videoPath = await cameraController?.stopVideoRecording();
|
||||
if (videoPath != null) {
|
||||
if (Platform.isAndroid) {
|
||||
// see https://github.com/flutter/flutter/issues/148335
|
||||
|
|
@ -432,7 +438,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
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;
|
||||
});
|
||||
// Get the position of the pointer
|
||||
RenderBox renderBox =
|
||||
keyTriggerButton.currentContext?.findRenderObject() as RenderBox;
|
||||
Offset localPosition =
|
||||
renderBox.globalToLocal(details.globalPosition);
|
||||
final renderBox =
|
||||
keyTriggerButton.currentContext!.findRenderObject()! as RenderBox;
|
||||
final localPosition = renderBox.globalToLocal(details.globalPosition);
|
||||
|
||||
final containerRect =
|
||||
Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height);
|
||||
|
|
@ -620,7 +625,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
height: 50,
|
||||
width: 80,
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Center(
|
||||
child: const Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.photoFilm,
|
||||
color: Colors.white,
|
||||
|
|
@ -653,7 +658,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!isVideoRecording) SizedBox(width: 80)
|
||||
if (!isVideoRecording) const SizedBox(width: 80)
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
|||
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(
|
||||
selectedCameraDetails, sCameraId, init, enableAudio);
|
||||
if (opts != null) {
|
||||
|
|
@ -41,7 +41,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
Future toggleSelectedCamera() async {
|
||||
Future<void> toggleSelectedCamera() async {
|
||||
await cameraController?.dispose();
|
||||
cameraController = null;
|
||||
selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class ImageItem {
|
|||
if (image != null) load(image);
|
||||
}
|
||||
|
||||
Future load(dynamic image) async {
|
||||
Future<void> load(dynamic image) async {
|
||||
loader = Completer<bool>();
|
||||
|
||||
if (image is ImageItem) {
|
||||
|
|
|
|||
|
|
@ -6,24 +6,23 @@ import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
|||
|
||||
/// Emoji layer
|
||||
class EmojiLayer extends StatefulWidget {
|
||||
const EmojiLayer({
|
||||
required this.layerData,
|
||||
super.key,
|
||||
this.onUpdate,
|
||||
});
|
||||
final EmojiLayerData layerData;
|
||||
final VoidCallback? onUpdate;
|
||||
|
||||
const EmojiLayer({
|
||||
super.key,
|
||||
required this.layerData,
|
||||
this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _EmojiLayerState();
|
||||
State<EmojiLayer> createState() => _EmojiLayerState();
|
||||
}
|
||||
|
||||
class _EmojiLayerState extends State<EmojiLayer> {
|
||||
double initialRotation = 0;
|
||||
Offset initialOffset = Offset.zero;
|
||||
Offset initialFocalPoint = Offset.zero;
|
||||
double initialScale = 1.0;
|
||||
double initialScale = 1;
|
||||
bool deleteLayer = false;
|
||||
bool twoPointerWhereDown = false;
|
||||
final GlobalKey outlineKey = GlobalKey();
|
||||
|
|
@ -94,16 +93,16 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
if (twoPointerWhereDown == true && details.pointerCount != 2) {
|
||||
return;
|
||||
}
|
||||
final RenderBox outlineBox =
|
||||
outlineKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final outlineBox =
|
||||
outlineKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
|
||||
final RenderBox emojiBox =
|
||||
emojiKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final emojiBox =
|
||||
emojiKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
|
||||
bool isAtTheBottom =
|
||||
final isAtTheBottom =
|
||||
(widget.layerData.offset.dy + emojiBox.size.height / 2) >
|
||||
outlineBox.size.height - 80;
|
||||
bool isInTheCenter = MediaQuery.of(context).size.width / 2 -
|
||||
final isInTheCenter = MediaQuery.of(context).size.width / 2 -
|
||||
30 <
|
||||
(widget.layerData.offset.dx +
|
||||
emojiBox.size.width / 2) &&
|
||||
|
|
@ -125,9 +124,9 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
initialRotation + details.rotation;
|
||||
|
||||
// Update the position based on the translation
|
||||
var dx = (initialOffset.dx) +
|
||||
final dx = (initialOffset.dx) +
|
||||
(details.focalPoint.dx - initialFocalPoint.dx);
|
||||
var dy = (initialOffset.dy) +
|
||||
final dy = (initialOffset.dy) +
|
||||
(details.focalPoint.dy - initialFocalPoint.dy);
|
||||
widget.layerData.offset = Offset(dx, dy);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import 'package:twonly/src/views/camera/image_editor/layers/filters/location_fil
|
|||
|
||||
/// Main layer
|
||||
class FilterLayer extends StatefulWidget {
|
||||
final FilterLayerData layerData;
|
||||
// final VoidCallback? onUpdate;
|
||||
|
||||
const FilterLayer({
|
||||
super.key,
|
||||
required this.layerData,
|
||||
super.key,
|
||||
// this.onUpdate,
|
||||
});
|
||||
final FilterLayerData layerData;
|
||||
|
||||
@override
|
||||
State<FilterLayer> createState() => _FilterLayerState();
|
||||
|
|
@ -26,7 +26,7 @@ class FilterSkeleton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
|
|
@ -39,8 +39,12 @@ class FilterSkeleton extends StatelessWidget {
|
|||
}
|
||||
|
||||
class FilterText extends StatelessWidget {
|
||||
const FilterText(this.text,
|
||||
{super.key, this.fontSize = 24, this.color = Colors.white});
|
||||
const FilterText(
|
||||
this.text, {
|
||||
super.key,
|
||||
this.fontSize = 24,
|
||||
this.color = Colors.white,
|
||||
});
|
||||
final String text;
|
||||
final double fontSize;
|
||||
final Color color;
|
||||
|
|
@ -53,9 +57,9 @@ class FilterText extends StatelessWidget {
|
|||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
color: color,
|
||||
shadows: [
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: const Color.fromARGB(122, 0, 0, 0),
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5.0,
|
||||
)
|
||||
],
|
||||
|
|
@ -67,10 +71,10 @@ class FilterText extends StatelessWidget {
|
|||
class _FilterLayerState extends State<FilterLayer> {
|
||||
final PageController pageController = PageController();
|
||||
List<Widget> pages = [
|
||||
FilterSkeleton(),
|
||||
DateTimeFilter(),
|
||||
LocationFilter(),
|
||||
FilterSkeleton(),
|
||||
const FilterSkeleton(),
|
||||
const DateTimeFilter(),
|
||||
const LocationFilter(),
|
||||
const FilterSkeleton(),
|
||||
];
|
||||
|
||||
@override
|
||||
|
|
@ -82,11 +86,11 @@ class _FilterLayerState extends State<FilterLayer> {
|
|||
initAsync();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
var stickers = (await getStickerIndex())
|
||||
.where((x) => x.imageSrc.contains("/imagefilter/"))
|
||||
.toList();
|
||||
stickers.sortBy((x) => x.imageSrc);
|
||||
Future<void> initAsync() async {
|
||||
final stickers = (await getStickerIndex())
|
||||
.where((x) => x.imageSrc.contains('/imagefilter/'))
|
||||
.toList()
|
||||
..sortBy((x) => x.imageSrc);
|
||||
|
||||
for (final sticker in stickers) {
|
||||
pages.insert(pages.length - 1, ImageFilter(imagePath: sticker.imageSrc));
|
||||
|
|
|
|||
|
|
@ -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/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.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/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/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 {
|
||||
const LocationFilter({super.key});
|
||||
|
|
@ -28,29 +29,30 @@ class _LocationFilterState extends State<LocationFilter> {
|
|||
initAsync();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
Future<void> initAsync() async {
|
||||
final res = await apiService.getCurrentLocation();
|
||||
if (res.isSuccess) {
|
||||
location = res.value.location;
|
||||
_searchForImage();
|
||||
// ignore: avoid_dynamic_calls
|
||||
location = res.value.location as Response_Location?;
|
||||
await _searchForImage();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _searchForImage() async {
|
||||
Future<void> _searchForImage() async {
|
||||
if (location == null) return;
|
||||
List<Sticker> imageIndex = await getStickerIndex();
|
||||
final imageIndex = await getStickerIndex();
|
||||
// Normalize the city and country for search
|
||||
String normalizedCity = location!.city.toLowerCase().replaceAll(' ', '_');
|
||||
String normalizedCountry = location!.county.toLowerCase();
|
||||
final normalizedCity = location!.city.toLowerCase().replaceAll(' ', '_');
|
||||
final normalizedCountry = location!.county.toLowerCase();
|
||||
|
||||
// Search for the city first
|
||||
for (var item in imageIndex) {
|
||||
for (final item in imageIndex) {
|
||||
if (item.imageSrc.contains('/cities/$normalizedCountry/')) {
|
||||
// Check if the item matches the normalized city
|
||||
if (item.imageSrc.endsWith('$normalizedCity.png')) {
|
||||
if (item.imageSrc.startsWith("/api/")) {
|
||||
_imageUrl = "https://twonly.eu/${item.imageSrc}";
|
||||
if (item.imageSrc.startsWith('/api/')) {
|
||||
_imageUrl = 'https://twonly.eu/${item.imageSrc}';
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
return;
|
||||
|
|
@ -60,11 +62,11 @@ class _LocationFilterState extends State<LocationFilter> {
|
|||
|
||||
// If city not found, search for the country
|
||||
if (_imageUrl == null) {
|
||||
for (var item in imageIndex) {
|
||||
for (final item in imageIndex) {
|
||||
if (item.imageSrc.contains('/countries/') &&
|
||||
item.imageSrc.contains(normalizedCountry)) {
|
||||
if (item.imageSrc.startsWith("/api/")) {
|
||||
_imageUrl = "https://twonly.eu/${item.imageSrc}";
|
||||
if (item.imageSrc.startsWith('/api/')) {
|
||||
_imageUrl = 'https://twonly.eu/${item.imageSrc}';
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
break;
|
||||
|
|
@ -91,7 +93,7 @@ class _LocationFilterState extends State<LocationFilter> {
|
|||
}
|
||||
|
||||
if (location != null) {
|
||||
if (location!.county != "-") {
|
||||
if (location!.county != '-') {
|
||||
return FilterSkeleton(
|
||||
child: Positioned(
|
||||
bottom: 50,
|
||||
|
|
@ -108,35 +110,35 @@ class _LocationFilterState extends State<LocationFilter> {
|
|||
}
|
||||
}
|
||||
|
||||
return DateTimeFilter(color: Colors.black);
|
||||
return const DateTimeFilter(color: Colors.black);
|
||||
}
|
||||
}
|
||||
|
||||
class Sticker {
|
||||
final String imageSrc;
|
||||
final String source;
|
||||
|
||||
Sticker({required this.imageSrc, required this.source});
|
||||
|
||||
factory Sticker.fromJson(Map<String, dynamic> json) {
|
||||
return Sticker(
|
||||
imageSrc: json['imageSrc'],
|
||||
source: json['source'] ?? '', // Handle null source
|
||||
imageSrc: json['imageSrc'] as String,
|
||||
source: json['source'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
final String imageSrc;
|
||||
final String source;
|
||||
}
|
||||
|
||||
Future<List<Sticker>> getStickerIndex() async {
|
||||
final directory = await getApplicationCacheDirectory();
|
||||
final indexFile = File('${directory.path}/stickers.json');
|
||||
List<Sticker> res = [];
|
||||
var res = <Sticker>[];
|
||||
|
||||
if (await indexFile.exists() && !kDebugMode) {
|
||||
final lastModified = await indexFile.lastModified();
|
||||
final difference = DateTime.now().difference(lastModified);
|
||||
final content = await indexFile.readAsString();
|
||||
List<dynamic> jsonList = json.decode(content);
|
||||
res = jsonList.map((json) => Sticker.fromJson(json)).toList();
|
||||
final jsonList = json.decode(content) as List;
|
||||
res = jsonList
|
||||
.map((json) => Sticker.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
if (difference.inHours < 2) {
|
||||
return res;
|
||||
}
|
||||
|
|
@ -146,13 +148,15 @@ Future<List<Sticker>> getStickerIndex() async {
|
|||
.get(Uri.parse('https://twonly.eu/api/sticker/stickers.json'));
|
||||
if (response.statusCode == 200) {
|
||||
await indexFile.writeAsString(response.body);
|
||||
List<dynamic> jsonList = json.decode(response.body);
|
||||
return jsonList.map((json) => Sticker.fromJson(json)).toList();
|
||||
final jsonList = json.decode(response.body) as List;
|
||||
return jsonList
|
||||
.map((json) => Sticker.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("$e");
|
||||
Log.error('$e');
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class _TextViewState extends State<TextLayer> {
|
|||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
),
|
||||
|
|
@ -182,7 +182,7 @@ class _TextViewState extends State<TextLayer> {
|
|||
child: Text(
|
||||
widget.layerData.text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,14 +8,13 @@ import 'package:twonly/src/views/camera/image_editor/layers/text_layer.dart';
|
|||
|
||||
/// View stacked layers (unbounded height, width)
|
||||
class LayersViewer extends StatelessWidget {
|
||||
final List<Layer> layers;
|
||||
final Function()? onUpdate;
|
||||
|
||||
const LayersViewer({
|
||||
super.key,
|
||||
required this.layers,
|
||||
super.key,
|
||||
this.onUpdate,
|
||||
});
|
||||
final List<Layer> layers;
|
||||
final void Function()? onUpdate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class _EmojisState extends State<Emojis> {
|
|||
initAsync();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
Future<void> initAsync() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
setState(() {
|
||||
|
|
@ -28,7 +28,7 @@ class _EmojisState extends State<Emojis> {
|
|||
});
|
||||
}
|
||||
|
||||
Future selectEmojis(String emoji) async {
|
||||
Future<void> selectEmojis(String emoji) async {
|
||||
await updateUserdata((user) {
|
||||
if (user.lastUsedEditorEmojis == null) {
|
||||
user.lastUsedEditorEmojis = [emoji];
|
||||
|
|
|
|||
|
|
@ -1,28 +1,31 @@
|
|||
// ignore_for_file: strict_raw_type
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.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/twonly_database.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/headline.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||
|
||||
class BestFriendsSelector extends StatelessWidget {
|
||||
final List<Contact> users;
|
||||
final Function(int, bool) updateStatus;
|
||||
final HashSet<int> selectedUserIds;
|
||||
final bool isRealTwonly;
|
||||
final String title;
|
||||
|
||||
const BestFriendsSelector(
|
||||
{super.key,
|
||||
const BestFriendsSelector({
|
||||
required this.users,
|
||||
required this.isRealTwonly,
|
||||
required this.updateStatus,
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -45,10 +48,11 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 4),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.outline.withAlpha(50),
|
||||
boxShadow: [
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
blurRadius: 10.9,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.1),
|
||||
|
|
@ -57,7 +61,7 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Text(context.lang.shareImagedSelectAll,
|
||||
style: TextStyle(fontSize: 10)),
|
||||
style: const TextStyle(fontSize: 10)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -81,8 +85,8 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
isRealTwonly: isRealTwonly,
|
||||
),
|
||||
),
|
||||
(secondUserIndex < users.length)
|
||||
? Expanded(
|
||||
if (secondUserIndex < users.length)
|
||||
Expanded(
|
||||
child: UserCheckbox(
|
||||
isChecked: selectedUserIds
|
||||
.contains(users[secondUserIndex].userId),
|
||||
|
|
@ -91,7 +95,8 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
isRealTwonly: isRealTwonly,
|
||||
),
|
||||
)
|
||||
: Expanded(
|
||||
else
|
||||
Expanded(
|
||||
child: Container(),
|
||||
),
|
||||
],
|
||||
|
|
@ -105,35 +110,34 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
}
|
||||
|
||||
class UserCheckbox extends StatelessWidget {
|
||||
final Contact user;
|
||||
final Function(int, bool) onChanged;
|
||||
final bool isChecked;
|
||||
final bool isRealTwonly;
|
||||
|
||||
const UserCheckbox({
|
||||
super.key,
|
||||
required this.user,
|
||||
required this.onChanged,
|
||||
required this.isRealTwonly,
|
||||
required this.isChecked,
|
||||
super.key,
|
||||
});
|
||||
final Contact user;
|
||||
final void Function(int, bool) onChanged;
|
||||
final bool isChecked;
|
||||
final bool isRealTwonly;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String displayName = getContactDisplayName(user);
|
||||
final displayName = getContactDisplayName(user);
|
||||
|
||||
return Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 3), // Padding inside the container
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 3), // Padding inside the container
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onChanged(user.userId, !isChecked);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.outline.withAlpha(50),
|
||||
boxShadow: [
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
blurRadius: 10.9,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.1),
|
||||
|
|
@ -147,16 +151,15 @@ class UserCheckbox extends StatelessWidget {
|
|||
contact: user,
|
||||
fontSize: 12,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (isRealTwonly)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 2),
|
||||
padding: const EdgeInsets.only(right: 2),
|
||||
child: VerifiedShield(
|
||||
user,
|
||||
size: 12,
|
||||
|
|
@ -186,10 +189,10 @@ class UserCheckbox extends StatelessWidget {
|
|||
side: WidgetStateBorderSide.resolveWith(
|
||||
(Set states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return BorderSide(width: 0);
|
||||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
width: 1, color: Theme.of(context).colorScheme.outline);
|
||||
color: Theme.of(context).colorScheme.outline);
|
||||
},
|
||||
),
|
||||
onChanged: (bool? value) {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
// ignore_for_file: inference_failure_on_function_invocation
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.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:screenshot/screenshot.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/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_view.dart';
|
||||
import 'package:flutter/services.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/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/layers_viewer.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:video_player/video_player.dart';
|
||||
|
||||
|
|
@ -34,13 +35,13 @@ const gMediaShowInfinite = 999999;
|
|||
|
||||
class ShareImageEditorView extends StatefulWidget {
|
||||
const ShareImageEditorView({
|
||||
required this.mirrorVideo,
|
||||
required this.useHighQuality,
|
||||
required this.sharedFromGallery,
|
||||
super.key,
|
||||
this.imageBytes,
|
||||
this.sendTo,
|
||||
this.videoFilePath,
|
||||
required this.mirrorVideo,
|
||||
required this.useHighQuality,
|
||||
required this.sharedFromGallery,
|
||||
});
|
||||
final Future<Uint8List?>? imageBytes;
|
||||
final File? videoFilePath;
|
||||
|
|
@ -87,9 +88,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
videoController?.initialize().then((_) {
|
||||
videoController!.play();
|
||||
setState(() {});
|
||||
}).catchError((Object error) {
|
||||
Log.error(error);
|
||||
});
|
||||
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
|
||||
}).catchError(Log.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future initMediaFileUpload() async {
|
||||
Future<void> initMediaFileUpload() async {
|
||||
// media init was already called...
|
||||
if (mediaUploadId != null) return;
|
||||
|
||||
|
|
@ -173,13 +173,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
Icons.add_reaction_outlined,
|
||||
tooltipText: context.lang.addEmoji,
|
||||
onPressed: () async {
|
||||
EmojiLayerData? layer = await showModalBottomSheet(
|
||||
final layer = await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (BuildContext context) {
|
||||
return const Emojis();
|
||||
},
|
||||
);
|
||||
) as Layer?;
|
||||
if (layer == null) return;
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
|
|
@ -190,9 +190,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
const SizedBox(height: 8),
|
||||
NotificationBadge(
|
||||
count: (widget.videoFilePath != null)
|
||||
? "0"
|
||||
? '0'
|
||||
: maxShowTime == 999999
|
||||
? "∞"
|
||||
? '∞'
|
||||
: maxShowTime.toString(),
|
||||
child: ActionButton(
|
||||
(widget.videoFilePath != null)
|
||||
|
|
@ -239,8 +239,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
onPressed: () async {
|
||||
if (widget.sendTo != null) {
|
||||
if (!widget.sendTo!.verified) {
|
||||
showAlertDialog(context, context.lang.shareImageUserNotVerified,
|
||||
context.lang.shareImageUserNotVerifiedDesc);
|
||||
await showAlertDialog(
|
||||
context,
|
||||
context.lang.shareImageUserNotVerified,
|
||||
context.lang.shareImageUserNotVerifiedDesc,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -277,9 +280,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
disable: layers.where((x) => !x.isDeleted).length <= 2,
|
||||
onPressed: () {
|
||||
if (removedLayers.isNotEmpty) {
|
||||
var lastLayer = removedLayers.removeLast();
|
||||
lastLayer.isDeleted = false;
|
||||
lastLayer.isEditing = false;
|
||||
final lastLayer = removedLayers.removeLast()
|
||||
..isDeleted = false
|
||||
..isEditing = false;
|
||||
layers.add(lastLayer);
|
||||
setState(() {});
|
||||
return;
|
||||
|
|
@ -308,15 +311,15 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
];
|
||||
}
|
||||
|
||||
Future pushShareImageView() async {
|
||||
Future<void> pushShareImageView() async {
|
||||
if (mediaUploadId == null) {
|
||||
await initMediaFileUpload();
|
||||
if (mediaUploadId == null) return;
|
||||
}
|
||||
Future<Uint8List?> imageBytes = getMergedImage();
|
||||
videoController?.pause();
|
||||
final imageBytes = getMergedImage();
|
||||
await videoController?.pause();
|
||||
if (isDisposed || !mounted) return;
|
||||
bool? wasSend = await Navigator.push(
|
||||
final wasSend = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageView(
|
||||
|
|
@ -330,12 +333,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
mirrorVideo: widget.mirrorVideo,
|
||||
),
|
||||
),
|
||||
);
|
||||
) as bool?;
|
||||
if (wasSend != null && wasSend && mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.pop(context, true);
|
||||
} else {
|
||||
videoController?.play();
|
||||
await videoController?.play();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,13 +346,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
Uint8List? image;
|
||||
|
||||
if (layers.length > 1 || widget.videoFilePath != null) {
|
||||
for (var x in layers) {
|
||||
for (final x in layers) {
|
||||
x.showCustomButtons = false;
|
||||
}
|
||||
setState(() {});
|
||||
image = await screenshotController.capture(
|
||||
pixelRatio: (widget.useHighQuality) ? pixelRatio : 1);
|
||||
for (var x in layers) {
|
||||
for (final x in layers) {
|
||||
x.showCustomButtons = true;
|
||||
}
|
||||
setState(() {});
|
||||
|
|
@ -362,7 +365,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}
|
||||
|
||||
Future<void> loadImage(Future<Uint8List?> imageFile) async {
|
||||
Uint8List? imageBytes = await imageFile;
|
||||
final imageBytes = await imageFile;
|
||||
await currentImage.load(imageBytes);
|
||||
if (isDisposed) return;
|
||||
|
||||
|
|
@ -380,19 +383,19 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
});
|
||||
}
|
||||
|
||||
Future sendImageToSinglePerson() async {
|
||||
Future<void> sendImageToSinglePerson() async {
|
||||
if (sendingOrLoadingImage) return;
|
||||
setState(() {
|
||||
sendingOrLoadingImage = true;
|
||||
});
|
||||
Uint8List? imageBytes = await getMergedImage();
|
||||
final imageBytes = await getMergedImage();
|
||||
if (!context.mounted) return;
|
||||
if (imageBytes == null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.pop(context, true);
|
||||
return;
|
||||
}
|
||||
ErrorCode? err = await isAllowedToSend();
|
||||
final err = await isAllowedToSend();
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (err != null) {
|
||||
|
|
@ -407,8 +410,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}));
|
||||
}
|
||||
} else {
|
||||
Future imageHandler =
|
||||
addOrModifyImageToUpload(mediaUploadId!, imageBytes);
|
||||
final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes);
|
||||
|
||||
// first finalize the upload
|
||||
await finalizeUpload(
|
||||
|
|
@ -421,7 +423,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
);
|
||||
|
||||
/// then call the upload process in the background
|
||||
encryptMediaFiles(
|
||||
await encryptMediaFiles(
|
||||
mediaUploadId!,
|
||||
imageHandler,
|
||||
videoUploadHandler,
|
||||
|
|
@ -526,7 +528,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
bottomNavigationBar: ColoredBox(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
|
|
@ -541,7 +543,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
displayButtonLabel: widget.sendTo == null,
|
||||
isLoading: loadingImage,
|
||||
),
|
||||
if (widget.sendTo != null) SizedBox(width: 10),
|
||||
if (widget.sendTo != null) const SizedBox(width: 10),
|
||||
if (widget.sendTo != null)
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
|
|
@ -549,7 +551,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: pushShareImageView,
|
||||
child: FaIcon(FontAwesomeIcons.userPlus),
|
||||
child: const FaIcon(FontAwesomeIcons.userPlus),
|
||||
),
|
||||
SizedBox(width: widget.sendTo == null ? 20 : 10),
|
||||
FilledButton.icon(
|
||||
|
|
@ -562,22 +564,22 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
color: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
)
|
||||
: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
if (sendingOrLoadingImage) return;
|
||||
if (widget.sendTo == null) return pushShareImageView();
|
||||
sendImageToSinglePerson();
|
||||
await sendImageToSinglePerson();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
(widget.sendTo == null)
|
||||
? context.lang.shareImagedEditorShareWith
|
||||
: getContactDisplayName(widget.sendTo!),
|
||||
style: TextStyle(fontSize: 17),
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
// ignore_for_file: strict_raw_type
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/utils/misc.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/headline.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.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';
|
||||
|
||||
class ShareImageView extends StatefulWidget {
|
||||
const ShareImageView({
|
||||
super.key,
|
||||
required this.imageBytesFuture,
|
||||
required this.isRealTwonly,
|
||||
required this.mirrorVideo,
|
||||
|
|
@ -27,6 +28,7 @@ class ShareImageView extends StatefulWidget {
|
|||
required this.updateStatus,
|
||||
required this.videoUploadHandler,
|
||||
required this.mediaUploadId,
|
||||
super.key,
|
||||
this.enableVideoAudio,
|
||||
});
|
||||
final Future<Uint8List?> imageBytesFuture;
|
||||
|
|
@ -36,7 +38,7 @@ class ShareImageView extends StatefulWidget {
|
|||
final HashSet<int> selectedUserIds;
|
||||
final bool? enableVideoAudio;
|
||||
final int mediaUploadId;
|
||||
final Function(int, bool) updateStatus;
|
||||
final void Function(int, bool) updateStatus;
|
||||
final Future<bool>? videoUploadHandler;
|
||||
|
||||
@override
|
||||
|
|
@ -60,8 +62,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Stream<List<Contact>> allContacts =
|
||||
twonlyDB.contactsDao.watchContactsForShareView();
|
||||
final allContacts = twonlyDB.contactsDao.watchContactsForShareView();
|
||||
|
||||
contactSub = allContacts.listen((allContacts) {
|
||||
setState(() {
|
||||
|
|
@ -73,13 +74,13 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
initAsync();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
Future<void> initAsync() async {
|
||||
imageBytes = await widget.imageBytesFuture;
|
||||
if (imageBytes != null) {
|
||||
final imageHandler =
|
||||
addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!);
|
||||
// start with the pre upload of the media file...
|
||||
encryptMediaFiles(
|
||||
await encryptMediaFiles(
|
||||
widget.mediaUploadId, imageHandler, widget.videoUploadHandler);
|
||||
}
|
||||
setState(() {});
|
||||
|
|
@ -91,12 +92,12 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
contactSub.cancel();
|
||||
}
|
||||
|
||||
Future updateUsers(List<Contact> users) async {
|
||||
Future<void> updateUsers(List<Contact> users) async {
|
||||
// Sort contacts by flameCounter and then by totalMediaCounter
|
||||
users.sort((a, b) {
|
||||
// First, compare by flameCounter
|
||||
int flameComparison = (getFlameCounterFromContact(b))
|
||||
.compareTo((getFlameCounterFromContact(a)));
|
||||
final flameComparison = getFlameCounterFromContact(b)
|
||||
.compareTo(getFlameCounterFromContact(a));
|
||||
if (flameComparison != 0) {
|
||||
return flameComparison; // Sort by flameCounter in descending order
|
||||
}
|
||||
|
|
@ -106,11 +107,11 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
});
|
||||
|
||||
// Separate best friends and other users
|
||||
List<Contact> bestFriends = [];
|
||||
List<Contact> otherUsers = [];
|
||||
List<Contact> pinnedContacts = users.where((c) => c.pinned).toList();
|
||||
final bestFriends = <Contact>[];
|
||||
final otherUsers = <Contact>[];
|
||||
final pinnedContacts = users.where((c) => c.pinned).toList();
|
||||
|
||||
for (var contact in users) {
|
||||
for (final contact in users) {
|
||||
if (contact.pinned) continue;
|
||||
if (!contact.archived &&
|
||||
(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;
|
||||
if (query.isEmpty) {
|
||||
updateUsers(contacts
|
||||
await updateUsers(contacts
|
||||
.where((x) =>
|
||||
!x.archived ||
|
||||
!hideArchivedUsers ||
|
||||
|
|
@ -139,17 +140,17 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
.toList());
|
||||
return;
|
||||
}
|
||||
List<Contact> usersFiltered = contacts
|
||||
final usersFiltered = contacts
|
||||
.where((user) => getContactDisplayName(user)
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase()))
|
||||
.toList();
|
||||
updateUsers(usersFiltered);
|
||||
await updateUsers(usersFiltered);
|
||||
}
|
||||
|
||||
void updateStatus(int userId, bool checked) {
|
||||
if (widget.isRealTwonly) {
|
||||
Contact user = contacts.firstWhere((x) => x.userId == userId);
|
||||
final user = contacts.firstWhere((x) => x.userId == userId);
|
||||
if (!user.verified) {
|
||||
showRealTwonlyWarning = true;
|
||||
setState(() {});
|
||||
|
|
@ -169,18 +170,19 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
),
|
||||
body: SafeArea(
|
||||
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(
|
||||
children: [
|
||||
if (showRealTwonlyWarning)
|
||||
Text(
|
||||
context.lang.shareImageAllTwonlyWarning,
|
||||
style: TextStyle(color: Colors.orange, fontSize: 13),
|
||||
style: const TextStyle(color: Colors.orange, fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (showRealTwonlyWarning) const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
onChanged: _filterUsers,
|
||||
decoration: getInputDecoration(
|
||||
|
|
@ -216,7 +218,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
children: [
|
||||
Text(
|
||||
context.lang.shareImageShowArchived,
|
||||
style: TextStyle(fontSize: 10),
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
Transform.scale(
|
||||
scale: 0.75,
|
||||
|
|
@ -225,10 +227,9 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
side: WidgetStateBorderSide.resolveWith(
|
||||
(Set states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return BorderSide(width: 0);
|
||||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline);
|
||||
|
|
@ -261,7 +262,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
floatingActionButton: SizedBox(
|
||||
height: 120,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
|
|
@ -275,13 +276,13 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
color: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
)
|
||||
: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
if (imageBytes == null || widget.selectedUserIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorCode? err = await isAllowedToSend();
|
||||
final err = await isAllowedToSend();
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (err != null) {
|
||||
|
|
@ -306,7 +307,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
);
|
||||
|
||||
/// trigger the upload of the media file.
|
||||
handleNextMediaUploadSteps(widget.mediaUploadId);
|
||||
unawaited(handleNextMediaUploadSteps(widget.mediaUploadId));
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
|
|
@ -321,7 +322,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
imageBytes == null || widget.selectedUserIds.isEmpty
|
||||
|
|
@ -330,7 +331,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
)),
|
||||
label: Text(
|
||||
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 {
|
||||
const UserList(
|
||||
this.users, {
|
||||
super.key,
|
||||
required this.selectedUserIds,
|
||||
required this.updateStatus,
|
||||
required this.isRealTwonly,
|
||||
super.key,
|
||||
});
|
||||
final Function(int, bool) updateStatus;
|
||||
final void Function(int, bool) updateStatus;
|
||||
final List<Contact> users;
|
||||
final bool isRealTwonly;
|
||||
final HashSet<int> selectedUserIds;
|
||||
|
|
@ -364,12 +365,10 @@ class UserList extends StatelessWidget {
|
|||
restorationId: 'new_message_users_list',
|
||||
itemCount: users.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
Contact user = users[i];
|
||||
int flameCounter = getFlameCounterFromContact(user);
|
||||
final user = users[i];
|
||||
final flameCounter = getFlameCounterFromContact(user);
|
||||
return ListTile(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start, // Center horizontally
|
||||
crossAxisAlignment: CrossAxisAlignment.center, // Center vertically
|
||||
children: [
|
||||
if (isRealTwonly)
|
||||
Padding(
|
||||
|
|
@ -394,10 +393,9 @@ class UserList extends StatelessWidget {
|
|||
side: WidgetStateBorderSide.resolveWith(
|
||||
(Set states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return BorderSide(width: 0);
|
||||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
width: 1, color: Theme.of(context).colorScheme.outline);
|
||||
return BorderSide(color: Theme.of(context).colorScheme.outline);
|
||||
},
|
||||
),
|
||||
onChanged: (bool? value) {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
// ignore_for_file: avoid_dynamic_calls
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.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/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/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/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/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/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';
|
||||
|
||||
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();
|
||||
if (user == null || user.username == searchUserName.text) {
|
||||
return;
|
||||
|
|
@ -84,23 +88,24 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
return;
|
||||
}
|
||||
|
||||
int added = await twonlyDB.contactsDao.insertContact(
|
||||
final added = await twonlyDB.contactsDao.insertContact(
|
||||
ContactsCompanion(
|
||||
username: Value(searchUserName.text),
|
||||
userId: Value(res.value.userdata.userId.toInt()),
|
||||
requested: Value(false),
|
||||
userId: Value(res.value.userdata.userId.toInt() as int),
|
||||
requested: const Value(false),
|
||||
),
|
||||
);
|
||||
|
||||
if (added > 0) {
|
||||
if (await createNewSignalSession(res.value.userdata)) {
|
||||
if (await createNewSignalSession(
|
||||
res.value.userdata as Response_UserData)) {
|
||||
// before notifying the other party, add
|
||||
await setupNotificationWithUsers(
|
||||
forceContact: res.value.userdata.userId.toInt(),
|
||||
forceContact: res.value.userdata.userId.toInt() as int,
|
||||
);
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
res.value.userdata.userId.toInt(),
|
||||
res.value.userdata.userId.toInt() as int,
|
||||
MessageJson(
|
||||
kind: MessageKind.contactRequest,
|
||||
timestamp: DateTime.now(),
|
||||
|
|
@ -111,7 +116,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
showAlertDialog(context, context.lang.searchUsernameNotFound,
|
||||
await showAlertDialog(context, context.lang.searchUsernameNotFound,
|
||||
context.lang.searchUsernameNotFoundBody(searchUserName.text));
|
||||
}
|
||||
setState(() {
|
||||
|
|
@ -119,59 +124,59 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
});
|
||||
}
|
||||
|
||||
InputDecoration getInputDecoration(hintText) {
|
||||
InputDecoration getInputDecoration(String hintText) {
|
||||
final primaryColor =
|
||||
Theme.of(context).colorScheme.primary; // Get the primary color
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
borderSide: BorderSide(color: primaryColor, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
borderSide: BorderSide(color: primaryColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isPreview = context.read<CustomChangeProvider>().plan == "Preview";
|
||||
final isPreview = context.read<CustomChangeProvider>().plan == 'Preview';
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.searchUsernameTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
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(
|
||||
children: [
|
||||
if (isPreview) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text(
|
||||
context.lang.searchUserNamePreview,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
FilledButton.icon(
|
||||
icon: FaIcon(FontAwesomeIcons.shieldHeart),
|
||||
icon: const FaIcon(FontAwesomeIcons.shieldHeart),
|
||||
onPressed: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return SubscriptionView();
|
||||
return const SubscriptionView();
|
||||
}));
|
||||
},
|
||||
label: Text(context.lang.selectSubscription),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
if (!isPreview) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
onSubmitted: (_) {
|
||||
_addNewUser(context);
|
||||
|
|
@ -184,7 +189,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
},
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(12),
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9A-Z]')),
|
||||
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')),
|
||||
],
|
||||
controller: searchUserName,
|
||||
decoration:
|
||||
|
|
@ -204,18 +209,18 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: (isPreview)
|
||||
floatingActionButton: isPreview
|
||||
? null
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30.0),
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: FloatingActionButton(
|
||||
foregroundColor: Colors.white,
|
||||
onPressed: () {
|
||||
if (!_isLoading) _addNewUser(context);
|
||||
},
|
||||
child: (_isLoading)
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: FaIcon(FontAwesomeIcons.magnifyingGlassPlus),
|
||||
: const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -237,9 +242,9 @@ class _ContactsListViewState extends State<ContactsListView> {
|
|||
Tooltip(
|
||||
message: context.lang.searchUserNameArchiveUserTooltip,
|
||||
child: IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.boxArchive, size: 15),
|
||||
icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15),
|
||||
onPressed: () async {
|
||||
final update = ContactsCompanion(archived: Value(true));
|
||||
const update = ContactsCompanion(archived: Value(true));
|
||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
||||
},
|
||||
),
|
||||
|
|
@ -253,10 +258,10 @@ class _ContactsListViewState extends State<ContactsListView> {
|
|||
Tooltip(
|
||||
message: context.lang.searchUserNameBlockUserTooltip,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.person_off_rounded,
|
||||
color: const Color.fromARGB(164, 244, 67, 54)),
|
||||
icon: const Icon(Icons.person_off_rounded,
|
||||
color: Color.fromARGB(164, 244, 67, 54)),
|
||||
onPressed: () async {
|
||||
final update = ContactsCompanion(blocked: Value(true));
|
||||
const update = ContactsCompanion(blocked: Value(true));
|
||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
||||
},
|
||||
),
|
||||
|
|
@ -264,17 +269,17 @@ class _ContactsListViewState extends State<ContactsListView> {
|
|||
Tooltip(
|
||||
message: context.lang.searchUserNameRejectUserTooltip,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.close, color: Colors.red),
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
onPressed: () async {
|
||||
rejectUser(contact.userId);
|
||||
await rejectUser(contact.userId);
|
||||
await deleteContact(contact.userId);
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.check, color: Colors.green),
|
||||
icon: const Icon(Icons.check, color: Colors.green),
|
||||
onPressed: () async {
|
||||
final update = ContactsCompanion(accepted: Value(true));
|
||||
const update = ContactsCompanion(accepted: Value(true));
|
||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
|
|
@ -286,7 +291,7 @@ class _ContactsListViewState extends State<ContactsListView> {
|
|||
),
|
||||
pushNotification: PushNotification(kind: PushKind.acceptRequest),
|
||||
);
|
||||
notifyContactsAboutProfileChange();
|
||||
await notifyContactsAboutProfileChange();
|
||||
},
|
||||
),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,30 +1,31 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.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/utils/misc.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/connection_info.comp.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/initialsavatar.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/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/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/tutorial/tutorials.dart';
|
||||
|
||||
|
|
@ -50,7 +51,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
Future<void> initAsync() async {
|
||||
final stream = twonlyDB.contactsDao.watchContactsForChatList();
|
||||
_contactsSub = stream.listen((contacts) {
|
||||
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;
|
||||
if (!mounted) return;
|
||||
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
|
||||
|
|
@ -86,15 +87,15 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isConnected = context.watch<CustomChangeProvider>().isConnected;
|
||||
final isConnected = context.watch<CustomChangeProvider>().isConnected;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(children: [
|
||||
Text("twonly "),
|
||||
const Text('twonly '),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return SubscriptionView();
|
||||
return const SubscriptionView();
|
||||
}));
|
||||
},
|
||||
child: Container(
|
||||
|
|
@ -102,7 +103,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
color: context.color.primary,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
|
||||
child: Text(
|
||||
context.watch<CustomChangeProvider>().plan,
|
||||
style: TextStyle(
|
||||
|
|
@ -121,13 +122,13 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ContactUsView(),
|
||||
builder: (context) => const ContactUsView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
color: Colors.grey,
|
||||
tooltip: context.lang.feedbackTooltip,
|
||||
icon: FaIcon(FontAwesomeIcons.commentDots, size: 19),
|
||||
icon: const FaIcon(FontAwesomeIcons.commentDots, size: 19),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: twonlyDB.contactsDao.watchContactsRequested(),
|
||||
|
|
@ -140,12 +141,12 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
count: count.toString(),
|
||||
child: IconButton(
|
||||
key: searchForOtherUsers,
|
||||
icon: FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
||||
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddNewUserView(),
|
||||
builder: (context) => const AddNewUserView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
@ -158,11 +159,11 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
Navigator.push(
|
||||
context,
|
||||
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,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: isConnected ? Container() : ConnectionInfo(),
|
||||
child: isConnected ? Container() : const ConnectionInfo(),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: (_contacts.isEmpty && _pinnedContacts.isEmpty)
|
||||
|
|
@ -180,12 +181,12 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: OutlinedButton.icon(
|
||||
icon: Icon(Icons.person_add),
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddNewUserView(),
|
||||
builder: (context) => const AddNewUserView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
@ -197,7 +198,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
onRefresh: () async {
|
||||
await apiService.close(() {});
|
||||
await apiService.connect(force: true);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: _pinnedContacts.length +
|
||||
|
|
@ -207,12 +208,12 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return BackupNoticeCard();
|
||||
return const BackupNoticeCard();
|
||||
}
|
||||
index -= 1;
|
||||
if (gIsDemoUser) {
|
||||
if (index == 0) {
|
||||
return DemoUserCard();
|
||||
return const DemoUserCard();
|
||||
}
|
||||
index -= 1;
|
||||
}
|
||||
|
|
@ -230,9 +231,9 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
}
|
||||
|
||||
// If there are pinned users, account for the Divider
|
||||
int adjustedIndex = index - _pinnedContacts.length;
|
||||
var adjustedIndex = index - _pinnedContacts.length;
|
||||
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) {
|
||||
return Divider();
|
||||
return const Divider();
|
||||
}
|
||||
|
||||
// Adjust the index for the contacts list
|
||||
|
|
@ -255,18 +256,18 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
],
|
||||
),
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30.0),
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: FloatingActionButton(
|
||||
foregroundColor: Colors.white,
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
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 {
|
||||
const UserListItem(
|
||||
{required this.user, required this.firstUserListItemKey, super.key});
|
||||
final Contact user;
|
||||
final GlobalKey? firstUserListItemKey;
|
||||
|
||||
const UserListItem(
|
||||
{super.key, required this.user, required this.firstUserListItemKey});
|
||||
|
||||
@override
|
||||
State<UserListItem> createState() => _UserListItem();
|
||||
}
|
||||
|
|
@ -363,11 +363,11 @@ class _UserListItem extends State<UserListItem> {
|
|||
|
||||
void lastUpdateTime() {
|
||||
// Change the color every 200 milliseconds
|
||||
updateTime = Timer.periodic(Duration(milliseconds: 200), (timer) {
|
||||
updateTime = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
setState(() {
|
||||
if (currentMessage != null) {
|
||||
lastMessageInSeconds =
|
||||
(DateTime.now().difference(currentMessage!.sendAt)).inSeconds;
|
||||
DateTime.now().difference(currentMessage!.sendAt).inSeconds;
|
||||
if (lastMessageInSeconds < 0) {
|
||||
lastMessageInSeconds = 0;
|
||||
}
|
||||
|
|
@ -378,7 +378,7 @@ class _UserListItem extends State<UserListItem> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int flameCounter = getFlameCounterFromContact(widget.user);
|
||||
final flameCounter = getFlameCounterFromContact(widget.user);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
|
|
@ -405,11 +405,11 @@ class _UserListItem extends State<UserListItem> {
|
|||
: Row(
|
||||
children: [
|
||||
MessageSendStateIcon(previewMessages),
|
||||
Text("•"),
|
||||
const Text('•'),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
formatDuration(lastMessageInSeconds),
|
||||
style: TextStyle(fontSize: 12),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
if (flameCounter > 0)
|
||||
FlameCounterWidget(
|
||||
|
|
@ -442,7 +442,7 @@ class _UserListItem extends State<UserListItem> {
|
|||
));
|
||||
return;
|
||||
}
|
||||
List<Message> msgs = previewMessages
|
||||
final msgs = previewMessages
|
||||
.where((x) => x.kind == MessageKind.media)
|
||||
.toList();
|
||||
if (msgs.isNotEmpty &&
|
||||
|
|
@ -452,6 +452,7 @@ class _UserListItem extends State<UserListItem> {
|
|||
switch (msgs.first.downloadState) {
|
||||
case DownloadState.pending:
|
||||
startDownloadMedia(msgs.first, true);
|
||||
return;
|
||||
case DownloadState.downloaded:
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
@ -459,9 +460,10 @@ class _UserListItem extends State<UserListItem> {
|
|||
return MediaViewerView(widget.user);
|
||||
}),
|
||||
);
|
||||
default:
|
||||
}
|
||||
return;
|
||||
case DownloadState.downloading:
|
||||
return;
|
||||
}
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
Future<void> initAsync() async {
|
||||
final user = await getUser();
|
||||
showBackupNotice = false;
|
||||
if (user != null &&
|
||||
|
|
@ -47,7 +47,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
|
|||
children: [
|
||||
Text(
|
||||
context.lang.backupNoticeTitle,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
|
@ -55,7 +55,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
|
|||
SizedBox(height: 5),
|
||||
Text(
|
||||
context.lang.backupNoticeDesc,
|
||||
style: TextStyle(fontSize: 14),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class DemoUserCard extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"This is a Demo-Preview.",
|
||||
'This is a Demo-Preview.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: !isDarkMode(context) ? Colors.white : Colors.black,
|
||||
|
|
@ -25,12 +25,12 @@ class DemoUserCard extends StatelessWidget {
|
|||
FilledButton(
|
||||
onPressed: () async {
|
||||
await deleteLocalUserData();
|
||||
Restart.restartApp(
|
||||
await Restart.restartApp(
|
||||
notificationTitle: 'Demo-Mode exited.',
|
||||
notificationBody: 'Click here to open the app again',
|
||||
);
|
||||
},
|
||||
child: Text("Register"),
|
||||
child: const Text('Register'),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,33 +2,33 @@ import 'dart:async';
|
|||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.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/services/api/messages.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/components/animate_icon.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/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/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/views/tutorial/tutorials.dart';
|
||||
|
||||
Color getMessageColor(Message message) {
|
||||
return (message.messageOtherId == null)
|
||||
? Color.fromARGB(255, 58, 136, 102)
|
||||
: Color.fromARGB(83, 68, 137, 255);
|
||||
? const Color.fromARGB(255, 58, 136, 102)
|
||||
: const Color.fromARGB(83, 68, 137, 255);
|
||||
}
|
||||
|
||||
/// Displays detailed information about a SampleItem.
|
||||
|
|
@ -45,7 +45,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
TextEditingController newMessageController = TextEditingController();
|
||||
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
||||
late Contact user;
|
||||
String currentInputText = "";
|
||||
String currentInputText = '';
|
||||
late StreamSubscription<Contact?> userSub;
|
||||
late StreamSubscription<List<Message>> messageSub;
|
||||
List<Message> messages = [];
|
||||
|
|
@ -64,7 +64,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
textFieldFocus = FocusNode();
|
||||
initStreams();
|
||||
|
||||
tutorial = Timer(Duration(seconds: 1), () async {
|
||||
tutorial = Timer(const Duration(seconds: 1), () async {
|
||||
tutorial = null;
|
||||
if (!mounted) return;
|
||||
await showVerifyShieldTutorial(context, verifyShieldKey);
|
||||
|
|
@ -80,10 +80,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
textFieldFocus.dispose();
|
||||
}
|
||||
|
||||
Future initStreams() async {
|
||||
Future<void> initStreams() async {
|
||||
await twonlyDB.messagesDao.removeOldMessages();
|
||||
Stream<Contact?> contact =
|
||||
twonlyDB.contactsDao.watchContact(widget.contact.userId);
|
||||
final contact = twonlyDB.contactsDao.watchContact(widget.contact.userId);
|
||||
userSub = contact.listen((contact) {
|
||||
if (contact == null) return;
|
||||
setState(() {
|
||||
|
|
@ -91,46 +90,48 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
});
|
||||
});
|
||||
|
||||
Stream<List<Message>> msgStream =
|
||||
final msgStream =
|
||||
twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId);
|
||||
messageSub = msgStream.listen((msgs) async {
|
||||
// if (!context.mounted) return;
|
||||
if (Platform.isAndroid) {
|
||||
flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
|
||||
await flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
|
||||
} else {
|
||||
flutterLocalNotificationsPlugin.cancelAll();
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
}
|
||||
List<Message> displayedMessages = [];
|
||||
final displayedMessages = <Message>[];
|
||||
// should be cleared
|
||||
Map<int, List<Message>> tmpTextReactionsToMessageId = {};
|
||||
Map<int, List<Message>> tmpEmojiReactionsToMessageId = {};
|
||||
final tmpTextReactionsToMessageId = <int, List<Message>>{};
|
||||
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...
|
||||
for (Message msg in msgs) {
|
||||
for (final msg in msgs) {
|
||||
if (msg.messageOtherId != null) {
|
||||
messageOtherMessageIdToMyMessageId[msg.messageOtherId!] =
|
||||
msg.messageId;
|
||||
}
|
||||
}
|
||||
|
||||
for (Message msg in msgs) {
|
||||
for (final msg in msgs) {
|
||||
if (msg.kind == MessageKind.textMessage &&
|
||||
msg.messageOtherId != null &&
|
||||
msg.openedAt == null) {
|
||||
openedMessageOtherIds.add(msg.messageOtherId!);
|
||||
}
|
||||
|
||||
int? responseId = msg.responseToMessageId ??
|
||||
final responseId = msg.responseToMessageId ??
|
||||
messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId];
|
||||
|
||||
if (responseId != null) {
|
||||
bool added = false;
|
||||
MessageContent? content =
|
||||
MessageContent.fromJson(msg.kind, jsonDecode(msg.contentJson!));
|
||||
var added = false;
|
||||
final content = MessageContent.fromJson(
|
||||
msg.kind,
|
||||
jsonDecode(msg.contentJson!) as Map,
|
||||
);
|
||||
if (content is TextMessageContent) {
|
||||
if (content.text.isNotEmpty && !isEmoji(content.text)) {
|
||||
added = true;
|
||||
|
|
@ -175,8 +176,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
});
|
||||
}
|
||||
|
||||
Future _sendMessage() async {
|
||||
if (newMessageController.text == "") return;
|
||||
Future<void> _sendMessage() async {
|
||||
if (newMessageController.text == '') return;
|
||||
|
||||
await sendTextMessage(
|
||||
user.userId,
|
||||
|
|
@ -197,7 +198,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
),
|
||||
);
|
||||
newMessageController.clear();
|
||||
currentInputText = "";
|
||||
currentInputText = '';
|
||||
responseToMessage = null;
|
||||
setState(() {});
|
||||
}
|
||||
|
|
@ -207,45 +208,44 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
|
||||
if (message.kind == MessageKind.textMessage) {
|
||||
if (message.contentJson != null) {
|
||||
MessageContent? content = MessageContent.fromJson(
|
||||
MessageKind.textMessage, jsonDecode(message.contentJson!));
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.textMessage, jsonDecode(message.contentJson!) as Map);
|
||||
if (content is TextMessageContent) {
|
||||
subtitle = truncateString(content.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.kind == MessageKind.media) {
|
||||
MessageContent? content = MessageContent.fromJson(
|
||||
MessageKind.media, jsonDecode(message.contentJson!));
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.media, jsonDecode(message.contentJson!) as Map);
|
||||
if (content is MediaMessageContent) {
|
||||
subtitle = content.isVideo ? "Video" : "Image";
|
||||
subtitle = content.isVideo ? 'Video' : 'Image';
|
||||
}
|
||||
}
|
||||
|
||||
String username = "You";
|
||||
var username = 'You';
|
||||
if (message.messageOtherId != null) {
|
||||
username = getContactDisplayName(widget.contact);
|
||||
}
|
||||
|
||||
Color color = getMessageColor(message);
|
||||
final color = getMessageColor(message);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: color,
|
||||
width: 2.0,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
username,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (subtitle != null) Text(subtitle)
|
||||
],
|
||||
|
|
@ -269,14 +269,14 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
contact: user,
|
||||
fontSize: 19,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(getContactDisplayName(user)),
|
||||
SizedBox(width: 10),
|
||||
const SizedBox(width: 10),
|
||||
VerifiedShield(key: verifyShieldKey, user),
|
||||
],
|
||||
),
|
||||
|
|
@ -300,8 +300,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
index -= 1;
|
||||
double size = 44;
|
||||
if (messages[index].kind == MessageKind.textMessage) {
|
||||
TextMessageContent? content = TextMessageContent.fromJson(
|
||||
jsonDecode(messages[index].contentJson!));
|
||||
final content = TextMessageContent.fromJson(
|
||||
jsonDecode(messages[index].contentJson!) as Map);
|
||||
if (EmojiAnimation.supported(content.text)) {
|
||||
size = 99;
|
||||
} else {
|
||||
|
|
@ -321,9 +321,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
if (reactions != null && reactions.isNotEmpty) {
|
||||
for (final reaction in reactions) {
|
||||
if (reaction.kind == MessageKind.textMessage) {
|
||||
TextMessageContent? content =
|
||||
TextMessageContent.fromJson(
|
||||
jsonDecode(reaction.contentJson!));
|
||||
final content = TextMessageContent.fromJson(
|
||||
jsonDecode(reaction.contentJson!) as Map);
|
||||
size += calculateNumberOfLines(content.text,
|
||||
MediaQuery.of(context).size.width * 0.5, 14) *
|
||||
27;
|
||||
|
|
@ -362,7 +361,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
if (responseToMessage != null && !user.deleted)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 00,
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
|
|
@ -376,7 +374,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
responseToMessage = null;
|
||||
});
|
||||
},
|
||||
icon: FaIcon(
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.xmark,
|
||||
size: 16,
|
||||
),
|
||||
|
|
@ -412,25 +410,23 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
decoration: inputTextMessageDeco(context),
|
||||
),
|
||||
),
|
||||
(currentInputText != "")
|
||||
? IconButton(
|
||||
padding: EdgeInsets.all(15),
|
||||
icon:
|
||||
FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () {
|
||||
_sendMessage();
|
||||
},
|
||||
if (currentInputText != '')
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(15),
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: _sendMessage,
|
||||
)
|
||||
: IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.camera),
|
||||
padding: EdgeInsets.all(15),
|
||||
else
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||
padding: const EdgeInsets.all(15),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(
|
||||
widget.contact);
|
||||
return CameraSendToView(widget.contact);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -469,7 +465,6 @@ double calculateNumberOfLines(String text, double width, double fontSize) {
|
|||
style: TextStyle(fontSize: fontSize),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout(maxWidth: (width - 32));
|
||||
)..layout(maxWidth: width - 32);
|
||||
return textPainter.computeLineMetrics().length.toDouble();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
checkIfTutorialCanBeShown();
|
||||
}
|
||||
|
||||
Future checkIfTutorialCanBeShown() async {
|
||||
Future<void> checkIfTutorialCanBeShown() async {
|
||||
if (widget.message.openedAt == null &&
|
||||
widget.message.messageOtherId != null ||
|
||||
widget.message.mediaStored) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import 'dart:convert';
|
||||
|
||||
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_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_response_columns.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 {
|
||||
const ChatListEntry(
|
||||
|
|
@ -17,8 +18,8 @@ class ChatListEntry extends StatefulWidget {
|
|||
this.lastMessageFromSameUser,
|
||||
this.textReactions,
|
||||
this.otherReactions, {
|
||||
super.key,
|
||||
required this.onResponseTriggered,
|
||||
super.key,
|
||||
});
|
||||
final Message message;
|
||||
final Contact contact;
|
||||
|
|
@ -26,7 +27,7 @@ class ChatListEntry extends StatefulWidget {
|
|||
final List<Message> textReactions;
|
||||
final List<Message> otherReactions;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final Function(Message) onResponseTriggered;
|
||||
final void Function(Message) onResponseTriggered;
|
||||
|
||||
@override
|
||||
State<ChatListEntry> createState() => _ChatListEntryState();
|
||||
|
|
@ -40,7 +41,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
final msgContent = MessageContent.fromJson(
|
||||
widget.message.kind, jsonDecode(widget.message.contentJson!));
|
||||
widget.message.kind, jsonDecode(widget.message.contentJson!) as Map);
|
||||
if (msgContent is TextMessageContent) {
|
||||
textMessage = msgContent.text;
|
||||
}
|
||||
|
|
@ -50,16 +51,14 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (content == null) return Container();
|
||||
bool right = widget.message.messageOtherId == null;
|
||||
final right = widget.message.messageOtherId == null;
|
||||
|
||||
return Container(
|
||||
// tag: "${widget.message.mediaUploadId ?? widget.message.messageId}",
|
||||
child: Align(
|
||||
return Align(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: widget.lastMessageFromSameUser
|
||||
? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10)
|
||||
: EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
|
||||
? const EdgeInsets.only(top: 5, right: 10, left: 10)
|
||||
: const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
right ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
|
|
@ -71,12 +70,13 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
child: Stack(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
children: [
|
||||
(textMessage != null)
|
||||
? ChatTextEntry(
|
||||
if (textMessage != null)
|
||||
ChatTextEntry(
|
||||
message: widget.message,
|
||||
text: textMessage!,
|
||||
)
|
||||
: ChatMediaEntry(
|
||||
else
|
||||
ChatMediaEntry(
|
||||
message: widget.message,
|
||||
contact: widget.contact,
|
||||
galleryItems: widget.galleryItems,
|
||||
|
|
@ -104,6 +104,6 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.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/model/json/message.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
|
||||
class ReactionRow extends StatefulWidget {
|
||||
const ReactionRow({
|
||||
super.key,
|
||||
required this.otherReactions,
|
||||
required this.message,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Message> otherReactions;
|
||||
|
|
@ -22,18 +23,18 @@ class ReactionRow extends StatefulWidget {
|
|||
class _ReactionRowState extends State<ReactionRow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> children = [];
|
||||
bool hasOneTextReaction = false;
|
||||
bool hasOneReopened = false;
|
||||
final children = <Widget>[];
|
||||
var hasOneTextReaction = false;
|
||||
var hasOneReopened = false;
|
||||
for (final reaction in widget.otherReactions) {
|
||||
MessageContent? content = MessageContent.fromJson(
|
||||
reaction.kind, jsonDecode(reaction.contentJson!));
|
||||
final content = MessageContent.fromJson(
|
||||
reaction.kind, jsonDecode(reaction.contentJson!) as Map);
|
||||
|
||||
if (content is ReopenedMediaFileContent) {
|
||||
if (hasOneReopened) continue;
|
||||
hasOneReopened = true;
|
||||
children.add(
|
||||
Expanded(
|
||||
const Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
|
|
@ -61,12 +62,12 @@ class _ReactionRowState extends State<ReactionRow> {
|
|||
child: EmojiAnimation(emoji: content.text),
|
||||
);
|
||||
} else {
|
||||
child = Text(content.text, style: TextStyle(fontSize: 14));
|
||||
child = Text(content.text, style: const TextStyle(fontSize: 14));
|
||||
}
|
||||
children.insert(
|
||||
0,
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 3),
|
||||
padding: const EdgeInsets.only(left: 3),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.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/model/json/message.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
|
||||
class ChatTextResponseColumns extends StatelessWidget {
|
||||
const ChatTextResponseColumns({
|
||||
super.key,
|
||||
required this.textReactions,
|
||||
required this.right,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Message> textReactions;
|
||||
|
|
@ -17,25 +18,25 @@ class ChatTextResponseColumns extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> children = [];
|
||||
final children = <Widget>[];
|
||||
for (final reaction in textReactions) {
|
||||
MessageContent? content = MessageContent.fromJson(
|
||||
reaction.kind, jsonDecode(reaction.contentJson!));
|
||||
final content = MessageContent.fromJson(
|
||||
reaction.kind, jsonDecode(reaction.contentJson!) as Map);
|
||||
|
||||
if (content is TextMessageContent) {
|
||||
var entries = [
|
||||
FaIcon(
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 10,
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
const SizedBox(width: 5),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.5,
|
||||
),
|
||||
child: Text(
|
||||
content.text,
|
||||
style: TextStyle(fontSize: 14),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
textAlign: right ? TextAlign.right : TextAlign.left,
|
||||
)),
|
||||
];
|
||||
|
|
@ -43,20 +44,21 @@ class ChatTextResponseColumns extends StatelessWidget {
|
|||
entries = entries.reversed.toList();
|
||||
}
|
||||
|
||||
Color color = getMessageColor(reaction);
|
||||
final color = getMessageColor(reaction);
|
||||
|
||||
children.insert(
|
||||
0,
|
||||
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(
|
||||
constraints: BoxConstraints(
|
||||
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(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.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/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_photo_slider.view.dart';
|
||||
|
||||
class InChatMediaViewer extends StatefulWidget {
|
||||
const InChatMediaViewer({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.contact,
|
||||
required this.color,
|
||||
required this.galleryItems,
|
||||
required this.canBeReopened,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
|
|
@ -40,9 +41,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
initStream();
|
||||
}
|
||||
|
||||
Future loadIndexAsync() async {
|
||||
Future<void> loadIndexAsync() async {
|
||||
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
|
||||
/// so using this timer as a workaround
|
||||
if (loadIndex()) {
|
||||
|
|
@ -72,7 +73,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
// videoController?.dispose();
|
||||
}
|
||||
|
||||
Future initStream() async {
|
||||
Future<void> initStream() async {
|
||||
/// 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
|
||||
|
||||
|
|
@ -85,22 +86,21 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
messageStream = stream.listen((updated) async {
|
||||
if (updated != null) {
|
||||
if (updated.mediaStored) {
|
||||
messageStream?.cancel();
|
||||
loadIndexAsync();
|
||||
await messageStream?.cancel();
|
||||
await loadIndexAsync();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future onTap() async {
|
||||
Future<void> onTap() async {
|
||||
if (galleryItemIndex == null) return;
|
||||
Navigator.push(
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MemoriesPhotoSliderView(
|
||||
galleryItems: widget.galleryItems,
|
||||
initialIndex: galleryItemIndex!,
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -110,15 +110,14 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
Widget build(BuildContext context) {
|
||||
if (galleryItemIndex == null) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 39,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: widget.color,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
|
|
@ -135,10 +134,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.transparent,
|
||||
width: 1.0,
|
||||
),
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: MemoriesItemThumbnail(
|
||||
galleryItem: widget.galleryItems[galleryItemIndex!],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_function_invocation
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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';
|
||||
|
||||
class MessageActions extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
const MessageActions({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.message,
|
||||
required this.onResponseTriggered,
|
||||
super.key,
|
||||
});
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
@override
|
||||
State<MessageActions> createState() => _SlidingResponseWidgetState();
|
||||
}
|
||||
|
||||
class _SlidingResponseWidgetState extends State<MessageActions> {
|
||||
double _offsetX = 0.0;
|
||||
double _offsetX = 0;
|
||||
bool gotFeedback = false;
|
||||
|
||||
void _onHorizontalDragUpdate(DragUpdateDetails details) {
|
||||
|
|
@ -77,7 +78,7 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
|
|||
),
|
||||
),
|
||||
if (_offsetX >= 40)
|
||||
Positioned(
|
||||
const Positioned(
|
||||
left: 20,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
|
|
@ -98,16 +99,15 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
|
|||
}
|
||||
|
||||
class MessageContextMenu extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
const MessageContextMenu({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.child,
|
||||
required this.onResponseTriggered,
|
||||
super.key,
|
||||
});
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -122,17 +122,17 @@ class MessageContextMenu extends StatelessWidget {
|
|||
PieAction(
|
||||
tooltip: Text(context.lang.react),
|
||||
onSelect: () async {
|
||||
EmojiLayerData? layer = await showModalBottomSheet(
|
||||
final layer = await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (BuildContext context) {
|
||||
return const Emojis();
|
||||
},
|
||||
);
|
||||
) as TextLayerData?;
|
||||
if (layer == null) return;
|
||||
Log.info(layer.text);
|
||||
|
||||
sendTextMessage(
|
||||
await sendTextMessage(
|
||||
message.contactId,
|
||||
TextMessageContent(
|
||||
text: layer.text,
|
||||
|
|
@ -171,7 +171,7 @@ class MessageContextMenu extends StatelessWidget {
|
|||
PieAction(
|
||||
tooltip: Text(context.lang.delete),
|
||||
onSelect: () async {
|
||||
bool delete = await showAlertDialog(
|
||||
final delete = await showAlertDialog(
|
||||
context,
|
||||
context.lang.deleteTitle,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
// ignore_for_file: inference_failure_on_collection_literal, avoid_dynamic_calls
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.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: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/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/notifications/background.notifications.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/storage.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';
|
||||
|
||||
final _noScreenshot = NoScreenshot.instance;
|
||||
final NoScreenshot _noScreenshot = NoScreenshot.instance;
|
||||
|
||||
class MediaViewerView extends StatefulWidget {
|
||||
final Contact contact;
|
||||
const MediaViewerView(this.contact, {super.key, this.initialMessage});
|
||||
final Contact contact;
|
||||
|
||||
final Message? initialMessage;
|
||||
|
||||
|
|
@ -89,17 +92,17 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future asyncLoadNextMedia(bool firstRun) async {
|
||||
Stream<List<Message>> messages =
|
||||
Future<void> asyncLoadNextMedia(bool firstRun) async {
|
||||
final messages =
|
||||
twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId);
|
||||
|
||||
_subscription = messages.listen((messages) {
|
||||
for (Message msg in messages) {
|
||||
for (final msg in messages) {
|
||||
// if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) {
|
||||
// allMediaFiles.add(msg);
|
||||
// }
|
||||
// Find the index of the existing message with the same messageId
|
||||
int index =
|
||||
final index =
|
||||
allMediaFiles.indexWhere((m) => m.messageId == msg.messageId);
|
||||
|
||||
if (index >= 1) {
|
||||
|
|
@ -114,24 +117,23 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
setState(() {});
|
||||
if (firstRun) {
|
||||
loadCurrentMediaFile();
|
||||
firstRun = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future nextMediaOrExit() async {
|
||||
Future<void> nextMediaOrExit() async {
|
||||
if (!mounted) return;
|
||||
videoController?.dispose();
|
||||
await videoController?.dispose();
|
||||
nextMediaTimer?.cancel();
|
||||
progressTimer?.cancel();
|
||||
if (allMediaFiles.isNotEmpty) {
|
||||
try {
|
||||
if (!imageSaved && maxShowTime != gMediaShowInfinite) {
|
||||
await deleteMediaFile(allMediaFiles.first.messageId, "mp4");
|
||||
await deleteMediaFile(allMediaFiles.first.messageId, "png");
|
||||
await deleteMediaFile(allMediaFiles.first.messageId, 'mp4');
|
||||
await deleteMediaFile(allMediaFiles.first.messageId, 'png');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("$e");
|
||||
Log.error('$e');
|
||||
}
|
||||
}
|
||||
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 (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit();
|
||||
await _noScreenshot.screenshotOff();
|
||||
|
|
@ -165,9 +167,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
});
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
flutterLocalNotificationsPlugin.cancel(allMediaFiles.first.contactId);
|
||||
await flutterLocalNotificationsPlugin
|
||||
.cancel(allMediaFiles.first.contactId);
|
||||
} else {
|
||||
flutterLocalNotificationsPlugin.cancelAll();
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
}
|
||||
|
||||
if (allMediaFiles.first.downloadState != DownloadState.downloaded) {
|
||||
|
|
@ -179,14 +182,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
final stream = twonlyDB.messagesDao
|
||||
.getMessageByMessageId(allMediaFiles.first.messageId)
|
||||
.watchSingleOrNull();
|
||||
downloadStateListener?.cancel();
|
||||
await downloadStateListener?.cancel();
|
||||
downloadStateListener = stream.listen((updated) async {
|
||||
if (updated != null) {
|
||||
if (updated.downloadState == DownloadState.downloaded) {
|
||||
downloadStateListener?.cancel();
|
||||
await downloadStateListener?.cancel();
|
||||
await handleNextDownloadedMedia(updated, showTwonly);
|
||||
// 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 {
|
||||
final MediaMessageContent content =
|
||||
MediaMessageContent.fromJson(jsonDecode(current.contentJson!));
|
||||
Future<void> handleNextDownloadedMedia(
|
||||
Message current, bool showTwonly) async {
|
||||
final content =
|
||||
MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map);
|
||||
|
||||
if (content.isRealTwonly) {
|
||||
setState(() {
|
||||
|
|
@ -205,12 +209,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
});
|
||||
if (!showTwonly) return;
|
||||
|
||||
bool isAuth = await authenticateUser(
|
||||
final isAuth = await authenticateUser(
|
||||
context.lang.mediaViewerAuthReason,
|
||||
force: false,
|
||||
);
|
||||
if (!isAuth) {
|
||||
nextMediaOrExit();
|
||||
await nextMediaOrExit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -229,8 +233,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
final videoPathTmp = await getVideoPath(current.messageId);
|
||||
if (videoPathTmp != null) {
|
||||
videoController = VideoPlayerController.file(File(videoPathTmp.path));
|
||||
videoController?.setLooping(content.maxShowTime == gMediaShowInfinite);
|
||||
videoController?.initialize().then((_) {
|
||||
await videoController
|
||||
?.setLooping(content.maxShowTime == gMediaShowInfinite);
|
||||
await videoController?.initialize().then((_) {
|
||||
videoController!.play();
|
||||
videoController?.addListener(() {
|
||||
setState(() {
|
||||
|
|
@ -248,9 +253,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
setState(() {
|
||||
videoPath = videoPathTmp.path;
|
||||
});
|
||||
}).catchError((Object error) {
|
||||
Log.error(error);
|
||||
});
|
||||
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
|
||||
}).catchError(Log.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +262,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
|
||||
if ((imageBytes == null && !content.isVideo) ||
|
||||
(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
|
||||
await handleMediaError(current);
|
||||
return nextMediaOrExit();
|
||||
|
|
@ -287,17 +291,17 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
nextMediaOrExit();
|
||||
}
|
||||
});
|
||||
progressTimer = Timer.periodic(Duration(milliseconds: 10), (timer) {
|
||||
progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
|
||||
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
|
||||
progress = (difference.inMilliseconds / (maxShowTime * 1000));
|
||||
progress = difference.inMilliseconds / (maxShowTime * 1000);
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future onPressedSaveToGallery() async {
|
||||
Future<void> onPressedSaveToGallery() async {
|
||||
if (allMediaFiles.first.messageOtherId == null) {
|
||||
return; // should not be possible
|
||||
}
|
||||
|
|
@ -306,7 +310,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
});
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
allMediaFiles.first.messageId,
|
||||
MessagesCompanion(mediaStored: Value(true)),
|
||||
const MessagesCompanion(mediaStored: Value(true)),
|
||||
);
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
|
|
@ -314,7 +318,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
MessageJson(
|
||||
kind: MessageKind.storedMediaFile,
|
||||
messageSenderId: allMediaFiles.first.messageId,
|
||||
messageReceiverId: allMediaFiles.first.messageOtherId!,
|
||||
messageReceiverId: allMediaFiles.first.messageOtherId,
|
||||
content: MessageContent(),
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
|
|
@ -337,8 +341,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
}
|
||||
|
||||
void displayShortReactions() {
|
||||
RenderBox renderBox =
|
||||
mediaWidgetKey.currentContext?.findRenderObject() as RenderBox;
|
||||
final renderBox =
|
||||
mediaWidgetKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
setState(() {
|
||||
showShortReactions = true;
|
||||
mediaViewerDistanceFromBottom = renderBox.size.height;
|
||||
|
|
@ -349,7 +353,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
return Row(
|
||||
key: mediaWidgetKey,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (maxShowTime == gMediaShowInfinite)
|
||||
OutlinedButton(
|
||||
|
|
@ -364,18 +367,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
onPressed: onPressedSaveToGallery,
|
||||
child: Row(
|
||||
children: [
|
||||
imageSaving
|
||||
? SizedBox(
|
||||
if (imageSaving)
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: CircularProgressIndicator(strokeWidth: 1))
|
||||
: imageSaved
|
||||
? Icon(Icons.check)
|
||||
: FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
else
|
||||
imageSaved
|
||||
? const Icon(Icons.check)
|
||||
: const FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
icon: SizedBox(
|
||||
width: 30,
|
||||
|
|
@ -410,13 +414,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
},
|
||||
style: ButtonStyle(
|
||||
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(
|
||||
icon: FaIcon(FontAwesomeIcons.message),
|
||||
icon: const FaIcon(FontAwesomeIcons.message),
|
||||
onPressed: () async {
|
||||
displayShortReactions();
|
||||
setState(() {
|
||||
|
|
@ -425,31 +429,32 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
},
|
||||
style: ButtonStyle(
|
||||
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(
|
||||
icon: FaIcon(FontAwesomeIcons.camera),
|
||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||
onPressed: () async {
|
||||
nextMediaTimer?.cancel();
|
||||
progressTimer?.cancel();
|
||||
videoController?.pause();
|
||||
await videoController?.pause();
|
||||
if (!mounted) return;
|
||||
await Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.contact);
|
||||
},
|
||||
));
|
||||
if (mounted && maxShowTime != gMediaShowInfinite) {
|
||||
nextMediaOrExit();
|
||||
await nextMediaOrExit();
|
||||
} else {
|
||||
videoController?.play();
|
||||
await videoController?.play();
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
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(
|
||||
imageBytes!,
|
||||
fit: BoxFit.contain,
|
||||
frameBuilder: ((context, child, frame,
|
||||
frameBuilder: (context, child, frame,
|
||||
wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded) return child;
|
||||
return AnimatedSwitcher(
|
||||
|
|
@ -505,12 +510,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
height: 60,
|
||||
color: Colors.transparent,
|
||||
width: 60,
|
||||
child: CircularProgressIndicator(
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -531,7 +536,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(bottom: 200),
|
||||
padding: const EdgeInsets.only(bottom: 200),
|
||||
child: Text(context.lang.mediaViewerTwonlyTapToOpen),
|
||||
),
|
||||
],
|
||||
|
|
@ -544,7 +549,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, size: 30),
|
||||
icon: const Icon(Icons.close, size: 30),
|
||||
color: Colors.white,
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
|
|
@ -554,7 +559,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
),
|
||||
if (isDownloading)
|
||||
Positioned.fill(
|
||||
const Positioned.fill(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
|
|
@ -574,7 +579,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
strokeWidth: 2.0,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -588,13 +593,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
child: Text(
|
||||
getContactDisplayName(widget.contact),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: const Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
@ -612,7 +617,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.xmark),
|
||||
icon: const FaIcon(FontAwesomeIcons.xmark),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showShortReactions = false;
|
||||
|
|
@ -634,7 +639,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () {
|
||||
if (textMessageController.text.isNotEmpty) {
|
||||
sendTextMessage(
|
||||
|
|
@ -684,7 +689,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
|
||||
class ReactionButtons extends StatefulWidget {
|
||||
const ReactionButtons({
|
||||
super.key,
|
||||
required this.show,
|
||||
required this.textInputFocused,
|
||||
required this.userId,
|
||||
|
|
@ -692,6 +696,7 @@ class ReactionButtons extends StatefulWidget {
|
|||
required this.responseToMessageId,
|
||||
required this.isVideo,
|
||||
required this.hide,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double mediaViewerDistanceFromBottom;
|
||||
|
|
@ -700,7 +705,7 @@ class ReactionButtons extends StatefulWidget {
|
|||
final bool textInputFocused;
|
||||
final int userId;
|
||||
final int responseToMessageId;
|
||||
final Function() hide;
|
||||
final void Function() hide;
|
||||
|
||||
@override
|
||||
State<ReactionButtons> createState() => _ReactionButtonsState();
|
||||
|
|
@ -718,8 +723,8 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
initAsync();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
var user = await getUser();
|
||||
Future<void> initAsync() async {
|
||||
final user = await getUser();
|
||||
if (user != null && user.preSelectedEmojies != null) {
|
||||
selectedEmojis = user.preSelectedEmojies!;
|
||||
}
|
||||
|
|
@ -733,7 +738,7 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : [];
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: Duration(milliseconds: 200), // Animation duration
|
||||
duration: const Duration(milliseconds: 200), // Animation duration
|
||||
bottom: widget.show
|
||||
? (widget.textInputFocused
|
||||
? 50
|
||||
|
|
@ -744,10 +749,11 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
curve: Curves.linearToEaseOut,
|
||||
child: AnimatedOpacity(
|
||||
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
|
||||
duration: Duration(milliseconds: 150),
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Container(
|
||||
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(
|
||||
children: [
|
||||
if (secondRowEmojis.isNotEmpty)
|
||||
|
|
@ -761,11 +767,11 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
hide: widget.hide,
|
||||
show: widget.show,
|
||||
isVideo: widget.isVideo,
|
||||
emoji: emoji,
|
||||
emoji: emoji as String,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
if (secondRowEmojis.isNotEmpty) SizedBox(height: 15),
|
||||
if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
|
@ -791,22 +797,21 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
}
|
||||
|
||||
class EmojiReactionWidget extends StatefulWidget {
|
||||
final int userId;
|
||||
final int responseToMessageId;
|
||||
final Function hide;
|
||||
final bool show;
|
||||
final bool isVideo;
|
||||
final String emoji;
|
||||
|
||||
const EmojiReactionWidget({
|
||||
super.key,
|
||||
required this.userId,
|
||||
required this.responseToMessageId,
|
||||
required this.hide,
|
||||
required this.isVideo,
|
||||
required this.show,
|
||||
required this.emoji,
|
||||
super.key,
|
||||
});
|
||||
final int userId;
|
||||
final int responseToMessageId;
|
||||
final Function hide;
|
||||
final bool show;
|
||||
final bool isVideo;
|
||||
final String emoji;
|
||||
|
||||
@override
|
||||
State<EmojiReactionWidget> createState() => _EmojiReactionWidgetState();
|
||||
|
|
@ -818,7 +823,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSize(
|
||||
duration: Duration(milliseconds: 200),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linearToEaseOut,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
|
|
@ -838,7 +843,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
|||
setState(() {
|
||||
selectedShortReaction = 0; // Assuming index is 0 for this example
|
||||
});
|
||||
Future.delayed(Duration(milliseconds: 300), () {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
widget.hide();
|
||||
selectedShortReaction = -1;
|
||||
|
|
@ -849,13 +854,13 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
|||
0) // Assuming index is 0 for this example
|
||||
? EmojiAnimationFlying(
|
||||
emoji: widget.emoji,
|
||||
duration: Duration(milliseconds: 300),
|
||||
startPosition: 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
startPosition: 0,
|
||||
size: (widget.show) ? 40 : 10,
|
||||
)
|
||||
: AnimatedOpacity(
|
||||
opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out
|
||||
duration: Duration(milliseconds: 150),
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: SizedBox(
|
||||
width: widget.show ? 40 : 10,
|
||||
child: Center(
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.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/twonly_database.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/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 {
|
||||
const StartNewChatView({super.key});
|
||||
|
|
@ -29,8 +30,7 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Stream<List<Contact>> stream =
|
||||
twonlyDB.contactsDao.watchContactsForStartNewChat();
|
||||
final stream = twonlyDB.contactsDao.watchContactsForStartNewChat();
|
||||
|
||||
contactSub = stream.listen((update) {
|
||||
update.sort((a, b) =>
|
||||
|
|
@ -48,14 +48,14 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
contactSub.cancel();
|
||||
}
|
||||
|
||||
Future filterUsers() async {
|
||||
Future<void> filterUsers() async {
|
||||
if (searchUserName.value.text.isEmpty) {
|
||||
setState(() {
|
||||
contacts = allContacts;
|
||||
});
|
||||
return;
|
||||
}
|
||||
List<Contact> usersFiltered = allContacts
|
||||
final usersFiltered = allContacts
|
||||
.where((user) => getContactDisplayName(user)
|
||||
.toLowerCase()
|
||||
.contains(searchUserName.value.text.toLowerCase()))
|
||||
|
|
@ -75,11 +75,12 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
child: PieCanvas(
|
||||
theme: getPieCanvasTheme(context),
|
||||
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(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
onChanged: (_) {
|
||||
filterUsers();
|
||||
|
|
@ -121,9 +122,9 @@ class UserList extends StatelessWidget {
|
|||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i == 0) {
|
||||
return ListTile(
|
||||
key: Key("add_new_contact"),
|
||||
key: const Key('add_new_contact'),
|
||||
title: Text(context.lang.startNewChatNewContact),
|
||||
leading: CircleAvatar(
|
||||
leading: const CircleAvatar(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.userPlus,
|
||||
size: 13,
|
||||
|
|
@ -133,24 +134,22 @@ class UserList extends StatelessWidget {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddNewUserView(),
|
||||
builder: (context) => const AddNewUserView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (i == 1) {
|
||||
return Divider();
|
||||
return const Divider();
|
||||
}
|
||||
Contact user = users[i - 2];
|
||||
int flameCounter = getFlameCounterFromContact(user);
|
||||
final user = users[i - 2];
|
||||
final flameCounter = getFlameCounterFromContact(user);
|
||||
return UserContextMenu(
|
||||
key: Key(user.userId.toString()),
|
||||
contact: user,
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(getContactDisplayName(user)),
|
||||
if (flameCounter >= 1)
|
||||
|
|
@ -159,14 +158,14 @@ class UserList extends StatelessWidget {
|
|||
flameCounter,
|
||||
prefix: true,
|
||||
),
|
||||
Spacer(),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.boxOpen,
|
||||
size: 13,
|
||||
color: user.archived ? null : Colors.transparent),
|
||||
onPressed: user.archived
|
||||
? () async {
|
||||
final update =
|
||||
const update =
|
||||
ContactsCompanion(archived: Value(false));
|
||||
await twonlyDB.contactsDao
|
||||
.updateContact(user.userId, update);
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ Future<bool> showAlertDialog(
|
|||
String? customOk,
|
||||
String? customCancel,
|
||||
}) async {
|
||||
Completer<bool> completer = Completer<bool>();
|
||||
final completer = Completer<bool>();
|
||||
|
||||
Widget okButton = TextButton(
|
||||
final Widget okButton = TextButton(
|
||||
child: Text(customOk ?? context.lang.ok),
|
||||
onPressed: () {
|
||||
completer.complete(true);
|
||||
|
|
@ -20,7 +20,7 @@ Future<bool> showAlertDialog(
|
|||
},
|
||||
);
|
||||
|
||||
Widget cancelButton = TextButton(
|
||||
final Widget cancelButton = TextButton(
|
||||
child: Text(customCancel ?? context.lang.cancel),
|
||||
onPressed: () {
|
||||
completer.complete(false);
|
||||
|
|
@ -29,7 +29,7 @@ Future<bool> showAlertDialog(
|
|||
);
|
||||
|
||||
// set up the AlertDialog
|
||||
AlertDialog alert = AlertDialog(
|
||||
final alert = AlertDialog(
|
||||
title: Text(title),
|
||||
content: (content == null) ? null : Text(content),
|
||||
actions: [
|
||||
|
|
@ -39,7 +39,8 @@ Future<bool> showAlertDialog(
|
|||
);
|
||||
|
||||
// show the dialog
|
||||
showDialog(
|
||||
// ignore: inference_failure_on_function_invocation
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
|
|
|
|||
|
|
@ -17,171 +17,170 @@ bool isEmoji(String character) {
|
|||
}
|
||||
|
||||
class EmojiAnimation extends StatelessWidget {
|
||||
const EmojiAnimation({required this.emoji, super.key, this.repeat = true});
|
||||
final String emoji;
|
||||
final bool repeat;
|
||||
static final Map<String, String> animatedIcons = {
|
||||
"❤": "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",
|
||||
'❤': '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',
|
||||
};
|
||||
|
||||
const EmojiAnimation({super.key, required this.emoji, this.repeat = true});
|
||||
|
||||
static bool supported(String emoji) {
|
||||
if (emoji.length > 4) return false;
|
||||
return animatedIcons.containsKey(emoji) || isEmoji(emoji);
|
||||
|
|
@ -194,39 +193,38 @@ class EmojiAnimation extends StatelessWidget {
|
|||
// Check if the emoji has a corresponding Lottie animation
|
||||
if (animatedIcons.containsKey(emoji)) {
|
||||
return Lottie.asset(
|
||||
"assets/animated_icons/${animatedIcons[emoji]}",
|
||||
'assets/animated_icons/${animatedIcons[emoji]}',
|
||||
repeat: repeat,
|
||||
);
|
||||
} else if (isEmoji(emoji)) {
|
||||
return Text(
|
||||
emoji,
|
||||
style: TextStyle(fontSize: 60),
|
||||
style: const TextStyle(fontSize: 60),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
emoji,
|
||||
style: TextStyle(fontSize: 15),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 Duration duration;
|
||||
final double startPosition;
|
||||
final int size;
|
||||
final bool repeat;
|
||||
|
||||
const EmojiAnimationFlying({
|
||||
super.key,
|
||||
required this.emoji,
|
||||
required this.duration,
|
||||
required this.startPosition,
|
||||
required this.size,
|
||||
this.repeat = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
|
|
|
|||
86
lib/src/views/components/app_outdated.dart
Normal file
86
lib/src/views/components/app_outdated.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
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 String text;
|
||||
final Widget? subtitle;
|
||||
|
|
@ -9,15 +17,6 @@ class BetterListTile extends StatelessWidget {
|
|||
final VoidCallback onTap;
|
||||
final double iconSize;
|
||||
|
||||
const BetterListTile(
|
||||
{super.key,
|
||||
required this.icon,
|
||||
required this.text,
|
||||
this.color,
|
||||
this.subtitle,
|
||||
required this.onTap,
|
||||
this.iconSize = 20});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
|
|
|
|||
|
|
@ -1,36 +1,34 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class BetterText extends StatelessWidget {
|
||||
const BetterText({required this.text, super.key});
|
||||
final String text;
|
||||
|
||||
const BetterText({super.key, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 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,}))',
|
||||
caseSensitive: false,
|
||||
multiLine: false,
|
||||
);
|
||||
|
||||
final List<TextSpan> spans = [];
|
||||
final Iterable<RegExpMatch> matches = urlRegExp.allMatches(text);
|
||||
final spans = <TextSpan>[];
|
||||
final matches = urlRegExp.allMatches(text);
|
||||
|
||||
int lastMatchEnd = 0;
|
||||
var lastMatchEnd = 0;
|
||||
|
||||
for (final match in matches) {
|
||||
if (match.start > lastMatchEnd) {
|
||||
spans.add(TextSpan(text: text.substring(lastMatchEnd, match.start)));
|
||||
}
|
||||
|
||||
final String? url = match.group(0);
|
||||
final url = match.group(0);
|
||||
spans.add(TextSpan(
|
||||
text: url,
|
||||
style: TextStyle(color: Colors.blue),
|
||||
style: const TextStyle(color: Colors.blue),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
final lUrl =
|
||||
|
|
@ -38,7 +36,7 @@ class BetterText extends StatelessWidget {
|
|||
try {
|
||||
await launchUrl(lUrl);
|
||||
} catch (e) {
|
||||
Log.error("Could not launch $e");
|
||||
Log.error('Could not launch $e');
|
||||
}
|
||||
},
|
||||
));
|
||||
|
|
@ -54,7 +52,7 @@ class BetterText extends StatelessWidget {
|
|||
TextSpan(
|
||||
children: spans,
|
||||
),
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 17,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FormattedStringWidget extends StatelessWidget {
|
||||
class FingerprintText extends StatelessWidget {
|
||||
const FingerprintText(this.longString, {super.key});
|
||||
final String longString;
|
||||
|
||||
const FormattedStringWidget(this.longString, {super.key});
|
||||
|
||||
String formatString(String input) {
|
||||
StringBuffer formattedString = StringBuffer();
|
||||
int blockCount = 0;
|
||||
final formattedString = StringBuffer();
|
||||
var blockCount = 0;
|
||||
|
||||
for (int i = 0; i < input.length; i += 4) {
|
||||
String block =
|
||||
for (var i = 0; i < input.length; i += 4) {
|
||||
final block =
|
||||
input.substring(i, i + 4 > input.length ? input.length : i + 4);
|
||||
formattedString.write(block);
|
||||
blockCount++;
|
||||
|
|
@ -30,7 +29,7 @@ class FormattedStringWidget extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return SelectableText(
|
||||
formatString(longString),
|
||||
style: TextStyle(fontSize: 16, color: Colors.black),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,26 +1,25 @@
|
|||
import 'package:flutter/material.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/views/components/animate_icon.dart';
|
||||
|
||||
class FlameCounterWidget extends StatelessWidget {
|
||||
final Contact user;
|
||||
final int flameCounter;
|
||||
final bool prefix;
|
||||
|
||||
const FlameCounterWidget(
|
||||
this.user,
|
||||
this.flameCounter, {
|
||||
this.prefix = false,
|
||||
super.key,
|
||||
});
|
||||
final Contact user;
|
||||
final int flameCounter;
|
||||
final bool prefix;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
if (prefix) const SizedBox(width: 5),
|
||||
if (prefix) Text("•"),
|
||||
if (prefix) const Text('•'),
|
||||
if (prefix) const SizedBox(width: 5),
|
||||
Text(
|
||||
flameCounter.toString(),
|
||||
|
|
@ -29,7 +28,7 @@ class FlameCounterWidget extends StatelessWidget {
|
|||
SizedBox(
|
||||
height: 15,
|
||||
child: EmojiAnimation(
|
||||
emoji: (globalBestFriendUserId == user.userId) ? "❤️🔥" : "🔥"),
|
||||
emoji: (globalBestFriendUserId == user.userId) ? '❤️🔥' : '🔥'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class HeadLineComponent extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const HeadLineComponent(this.text, {super.key});
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 10),
|
||||
child: 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
Loading…
Reference in a new issue