This commit is contained in:
otsmr 2025-07-18 23:43:22 +02:00
parent 9f45a461a2
commit eb545f84b0
21 changed files with 190 additions and 122 deletions

View file

@ -4,7 +4,7 @@ on:
workflow_dispatch: {}
push:
branches:
- main
- main_disabled
# paths:
# - lib/**
# - pubspec.lock

View file

@ -5,7 +5,7 @@
- twonly now has a free plan and is now financed by donations and an optional subscription with more features (coming soon)
- iOS gestures to close images
- Improved chat messages view, including better citation view and display times
- onboarding screens updated and registration view simplified
- Onboarding screens updated and registration view simplified
- The sender is displayed in the top right corner when a media file is opened
- Images are now stored as WebP to save storage
- Button to report users

View file

@ -19,6 +19,7 @@ void Function({required bool isConnected}) globalCallbackConnectionState = ({
required bool isConnected,
}) {};
void Function() globalCallbackAppIsOutdated = () {};
void Function() globalCallbackNewDeviceRegistered = () {};
void Function(String planId) globalCallbackUpdatePlan = (String planId) {};
bool globalIsAppInBackground = true;

View file

@ -334,5 +334,6 @@
"openChangeLog": "Changelog automatisch öffnen",
"reportUserTitle": "Melde {username}",
"reportUserReason": "Meldegrund",
"reportUser": "Benutzer melden"
"reportUser": "Benutzer melden",
"newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet."
}

View file

@ -490,5 +490,6 @@
"openChangeLog": "Open changelog automatically",
"reportUserTitle": "Report {username}",
"reportUserReason": "Reporting reason",
"reportUser": "Report user"
"reportUser": "Report user",
"newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here."
}

View file

@ -2047,6 +2047,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Report user'**
String get reportUser;
/// No description provided for @newDeviceRegistered.
///
/// In en, this message translates to:
/// **'You have logged in on another device. You have therefore been logged out here.'**
String get newDeviceRegistered;
}
class _AppLocalizationsDelegate

View file

@ -1087,4 +1087,8 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get reportUser => 'Benutzer melden';
@override
String get newDeviceRegistered =>
'Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.';
}

View file

@ -1081,4 +1081,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get reportUser => 'Report user';
@override
String get newDeviceRegistered =>
'You have logged in on another device. You have therefore been logged out here.';
}

View file

@ -29,9 +29,12 @@ class UserData {
@JsonKey(defaultValue: 0)
int avatarCounter = 0;
@JsonKey(defaultValue: 0)
int deviceId = 0;
// --- SUBSCRIPTION DTA ---
@JsonKey(defaultValue: 'Preview')
@JsonKey(defaultValue: 'Free')
String subscriptionPlan;
DateTime? lastImageSend;
int? todaysImageCounter;

View file

@ -10,12 +10,13 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
userId: (json['userId'] as num).toInt(),
username: json['username'] as String,
displayName: json['displayName'] as String,
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Preview',
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
isDemoUser: json['isDemoUser'] as bool? ?? false,
)
..avatarSvg = json['avatarSvg'] as String?
..avatarJson = json['avatarJson'] as String?
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
..lastImageSend = json['lastImageSend'] == null
? null
: DateTime.parse(json['lastImageSend'] as String)
@ -76,6 +77,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson,
'avatarCounter': instance.avatarCounter,
'deviceId': instance.deviceId,
'subscriptionPlan': instance.subscriptionPlan,
'lastImageSend': instance.lastImageSend?.toIso8601String(),
'todaysImageCounter': instance.todaysImageCounter,

View file

@ -445,6 +445,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
$fixnum.Int64? userId,
$core.List<$core.int>? authToken,
$core.String? appVersion,
$fixnum.Int64? deviceId,
}) {
final $result = create();
if (userId != null) {
@ -456,6 +457,9 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
if (appVersion != null) {
$result.appVersion = appVersion;
}
if (deviceId != null) {
$result.deviceId = deviceId;
}
return $result;
}
Handshake_Authenticate._() : super();
@ -466,6 +470,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
..aInt64(1, _omitFieldNames ? '' : 'userId')
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'authToken', $pb.PbFieldType.OY)
..aOS(3, _omitFieldNames ? '' : 'appVersion')
..aInt64(4, _omitFieldNames ? '' : 'deviceId')
..hasRequiredFields = false
;
@ -516,6 +521,15 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
$core.bool hasAppVersion() => $_has(2);
@$pb.TagNumber(3)
void clearAppVersion() => clearField(3);
@$pb.TagNumber(4)
$fixnum.Int64 get deviceId => $_getI64(3);
@$pb.TagNumber(4)
set deviceId($fixnum.Int64 v) { $_setInt64(3, v); }
@$pb.TagNumber(4)
$core.bool hasDeviceId() => $_has(3);
@$pb.TagNumber(4)
void clearDeviceId() => clearField(4);
}
enum Handshake_Handshake {

View file

@ -106,9 +106,11 @@ const Handshake_Authenticate$json = {
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{'1': 'auth_token', '3': 2, '4': 1, '5': 12, '10': 'authToken'},
{'1': 'app_version', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'appVersion', '17': true},
{'1': 'device_id', '3': 4, '4': 1, '5': 3, '9': 1, '10': 'deviceId', '17': true},
],
'8': [
{'1': '_app_version'},
{'1': '_device_id'},
],
};
@ -128,9 +130,10 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'lkGAcgASgDUg5yZWdpc3RyYXRpb25JZBIaCgZpc19pb3MYCCABKAhIAVIFaXNJb3OIAQFCDgoM'
'X2ludml0ZV9jb2RlQgkKB19pc19pb3MaEgoQR2V0QXV0aENoYWxsZW5nZRpDCgxHZXRBdXRoVG'
'9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25z'
'ZRp8CgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW'
'4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVyc2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEB'
'Qg4KDF9hcHBfdmVyc2lvbkILCglIYW5kc2hha2U=');
'ZRqsAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2'
'VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3ZlcnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogB'
'ARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aWNlSWSIAQFCDgoMX2FwcF92ZXJzaW9uQgwKCl'
'9kZXZpY2VfaWRCCwoJSGFuZHNoYWtl');
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData$json = {

View file

@ -47,6 +47,7 @@ class ErrorCode extends $pb.ProtobufEnum {
static const ErrorCode UserIdNotFound = ErrorCode._(1028, _omitEnumNames ? '' : 'UserIdNotFound');
static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken');
static const ErrorCode AppVersionOutdated = ErrorCode._(1030, _omitEnumNames ? '' : 'AppVersionOutdated');
static const ErrorCode NewDeviceRegistered = ErrorCode._(1031, _omitEnumNames ? '' : 'NewDeviceRegistered');
static const $core.List<ErrorCode> values = <ErrorCode> [
Unknown,
@ -82,6 +83,7 @@ class ErrorCode extends $pb.ProtobufEnum {
UserIdNotFound,
UserIdAlreadyTaken,
AppVersionOutdated,
NewDeviceRegistered,
];
static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values);

View file

@ -50,6 +50,7 @@ const ErrorCode$json = {
{'1': 'UserIdNotFound', '2': 1028},
{'1': 'UserIdAlreadyTaken', '2': 1029},
{'1': 'AppVersionOutdated', '2': 1030},
{'1': 'NewDeviceRegistered', '2': 1031},
],
};
@ -69,5 +70,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q'
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCA==');
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcI');

View file

@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
bool _isConnected = false;
bool get isConnected => _isConnected;
String plan = 'Preview';
String plan = 'Free';
Future<void> updateConnectionState(bool update) async {
_isConnected = update;
notifyListeners();

View file

@ -316,6 +316,13 @@ class ApiService {
await close(() {});
return Result.error(ErrorCode.InternalError);
}
if (res.error == ErrorCode.NewDeviceRegistered) {
globalCallbackNewDeviceRegistered();
Log.error('Device is disabled, as a newer device restore twonly Safe.');
appIsOutdated = true;
await close(() {});
return Result.error(ErrorCode.InternalError);
}
if (res.error == ErrorCode.SessionNotAuthenticated) {
isAuthenticated = false;
if (authenticated) {
@ -346,11 +353,13 @@ class ApiService {
const storage = FlutterSecureStorage();
final apiAuthToken =
await storage.read(key: SecureStorageKeys.apiAuthToken);
final user = await getUser();
if (apiAuthToken != null) {
if (apiAuthToken != null && user != null) {
final authenticate = Handshake_Authenticate()
..userId = Int64(userId)
..appVersion = (await PackageInfo.fromPlatform()).version
..deviceId = Int64(user.deviceId)
..authToken = base64Decode(apiAuthToken);
final handshake = Handshake()..authenticate = authenticate;

View file

@ -36,9 +36,6 @@ import 'package:video_compress/video_compress.dart';
Future<ErrorCode?> isAllowedToSend() async {
final user = await getUser();
if (user == null) return null;
if (user.subscriptionPlan == 'Preview') {
return ErrorCode.PlanNotAllowed;
}
if (user.subscriptionPlan == 'Free') {
var todaysImageCounter = user.todaysImageCounter;
if (user.lastImageSend != null && user.todaysImageCounter != null) {

View file

@ -16,6 +16,7 @@ import 'package:twonly/src/model/json/userdata.dart';
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';
import 'package:twonly/src/utils/storage.dart';
Future<void> recoverTwonlySafe(
String username,
@ -136,4 +137,8 @@ Future<void> handleBackupData(
await storage.write(
key: SecureStorageKeys.userData,
value: secureStorage[SecureStorageKeys.userData] as String);
await updateUserdata((u) {
u.deviceId += 1;
return u;
});
}

View file

@ -48,7 +48,8 @@ Future<void> updateUsersPlan(BuildContext context, String planId) async {
Mutex updateProtection = Mutex();
Future<UserData?> updateUserdata(
UserData Function(UserData userData) updateUser) async {
UserData Function(UserData userData) updateUser,
) async {
return updateProtection.protect<UserData?>(() async {
final user = await getUser();
if (user == null) return null;

View file

@ -6,7 +6,6 @@ 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/globals.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
@ -14,7 +13,6 @@ 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';
@ -24,7 +22,6 @@ 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/views/settings/subscription/subscription.view.dart';
class AddNewUserView extends StatefulWidget {
const AddNewUserView({super.key});
@ -143,7 +140,6 @@ class _SearchUsernameView extends State<AddNewUserView> {
@override
Widget build(BuildContext context) {
final isPreview = context.read<CustomChangeProvider>().plan == 'Preview';
return Scaffold(
appBar: AppBar(
title: Text(context.lang.searchUsernameTitle),
@ -154,27 +150,6 @@ class _SearchUsernameView extends State<AddNewUserView> {
const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
child: Column(
children: [
if (isPreview) ...[
Padding(
padding: const EdgeInsets.all(20),
child: Text(
context.lang.searchUserNamePreview,
textAlign: TextAlign.center,
),
),
FilledButton.icon(
icon: const FaIcon(FontAwesomeIcons.shieldHeart),
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const SubscriptionView();
}));
},
label: Text(context.lang.selectSubscription),
),
const SizedBox(height: 30),
],
if (!isPreview) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField(
@ -196,7 +171,6 @@ class _SearchUsernameView extends State<AddNewUserView> {
getInputDecoration(context.lang.searchUsernameInput),
),
),
],
const SizedBox(height: 20),
if (contacts.isNotEmpty)
HeadLineComponent(
@ -209,9 +183,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
),
),
),
floatingActionButton: isPreview
? null
: Padding(
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton(
foregroundColor: Colors.white,

View file

@ -15,25 +15,65 @@ class AppOutdated extends StatefulWidget {
class _AppOutdatedState extends State<AppOutdated> {
bool appIsOutdated = false;
bool newDeviceRegistered = false;
@override
void dispose() {
globalCallbackAppIsOutdated = () {};
globalCallbackNewDeviceRegistered = () {};
super.dispose();
}
Future<void> initAsync() async {
@override
void initState() {
globalCallbackAppIsOutdated = () async {
await context.read<CustomChangeProvider>().updateConnectionState(false);
setState(() {
appIsOutdated = true;
});
};
globalCallbackNewDeviceRegistered = () async {
await context.read<CustomChangeProvider>().updateConnectionState(false);
setState(() {
newDeviceRegistered = true;
});
};
super.initState();
}
@override
Widget build(BuildContext context) {
if (!appIsOutdated) return Container();
if (newDeviceRegistered) {
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.withAlpha(100),
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
context.lang.newDeviceRegistered,
textAlign: TextAlign.center,
softWrap: true,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
],
),
),
),
);
}
if (appIsOutdated) {
return Positioned(
top: 60,
left: 30,
@ -83,4 +123,6 @@ class _AppOutdatedState extends State<AppOutdated> {
),
);
}
return Container();
}
}