fix: скрытие удаленных аккаунтов с контактов, возможно фикс скрытия медиа дорожки в кружках на кастомных android прошивках версии android ниже 16
This commit is contained in:
@@ -21,6 +21,7 @@ class CachedContact {
|
|||||||
final String? baseRawUrl;
|
final String? baseRawUrl;
|
||||||
final int updateTime;
|
final int updateTime;
|
||||||
final Set<String> options;
|
final Set<String> options;
|
||||||
|
final int accountStatus;
|
||||||
|
|
||||||
const CachedContact({
|
const CachedContact({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -33,12 +34,14 @@ class CachedContact {
|
|||||||
this.baseRawUrl,
|
this.baseRawUrl,
|
||||||
required this.updateTime,
|
required this.updateTime,
|
||||||
this.options = const {},
|
this.options = const {},
|
||||||
|
this.accountStatus = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isOfficial => options.contains('OFFICIAL');
|
bool get isOfficial => options.contains('OFFICIAL');
|
||||||
bool get isBot => options.contains('BOT');
|
bool get isBot => options.contains('BOT');
|
||||||
bool get isServiceAccount => options.contains('SERVICE_ACCOUNT');
|
bool get isServiceAccount => options.contains('SERVICE_ACCOUNT');
|
||||||
bool get isVerified => isOfficial;
|
bool get isVerified => isOfficial;
|
||||||
|
bool get isDeleted => accountStatus != 0;
|
||||||
|
|
||||||
factory CachedContact.fromDbRow(Map<String, dynamic> row) => CachedContact(
|
factory CachedContact.fromDbRow(Map<String, dynamic> row) => CachedContact(
|
||||||
id: row['id'] as int,
|
id: row['id'] as int,
|
||||||
@@ -51,6 +54,7 @@ class CachedContact {
|
|||||||
baseRawUrl: row['base_raw_url'] as String?,
|
baseRawUrl: row['base_raw_url'] as String?,
|
||||||
updateTime: row['update_time'] as int,
|
updateTime: row['update_time'] as int,
|
||||||
options: _decodeOptions(row['options']),
|
options: _decodeOptions(row['options']),
|
||||||
|
accountStatus: (row['account_status'] as int?) ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Set<String> _decodeOptions(dynamic raw) {
|
static Set<String> _decodeOptions(dynamic raw) {
|
||||||
@@ -183,6 +187,7 @@ class ContactsModule {
|
|||||||
'base_raw_url': null,
|
'base_raw_url': null,
|
||||||
'update_time': 0,
|
'update_time': 0,
|
||||||
'options': null,
|
'options': null,
|
||||||
|
'account_status': 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (row == null) return null;
|
if (row == null) return null;
|
||||||
@@ -397,7 +402,14 @@ class ContactsModule {
|
|||||||
int accountId,
|
int accountId,
|
||||||
) async {
|
) async {
|
||||||
final contacts = data['contacts'];
|
final contacts = data['contacts'];
|
||||||
if (contacts is! List || contacts.isEmpty) return;
|
if (contacts is! List) {
|
||||||
|
logger.i('Контакты: сервер не прислал список (акк $accountId)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (contacts.isEmpty) {
|
||||||
|
logger.i('Контакты: сервер прислал пустой список (акк $accountId)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final rows = <Map<String, dynamic>>[];
|
final rows = <Map<String, dynamic>>[];
|
||||||
for (final raw in contacts.whereType<Map>()) {
|
for (final raw in contacts.whereType<Map>()) {
|
||||||
@@ -492,8 +504,14 @@ class ContactsModule {
|
|||||||
return photos;
|
return photos;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<CachedContact>> getContacts(int accountId) async {
|
static Future<List<CachedContact>> getContacts(
|
||||||
final rows = await AppDatabase.loadContacts(accountId);
|
int accountId, {
|
||||||
|
bool includeDeleted = false,
|
||||||
|
}) async {
|
||||||
|
final rows = await AppDatabase.loadContacts(
|
||||||
|
accountId,
|
||||||
|
includeDeleted: includeDeleted,
|
||||||
|
);
|
||||||
return rows.map(CachedContact.fromDbRow).toList();
|
return rows.map(CachedContact.fromDbRow).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,7 +589,7 @@ class ContactsModule {
|
|||||||
/// Прогревает in-memory ContactCache из локальных контактов.
|
/// Прогревает in-memory ContactCache из локальных контактов.
|
||||||
/// Нужно вызывать на cold start: иначе кэш пуст до следующего логина.
|
/// Нужно вызывать на cold start: иначе кэш пуст до следующего логина.
|
||||||
static Future<void> primeCacheFromDb(int accountId) async {
|
static Future<void> primeCacheFromDb(int accountId) async {
|
||||||
final contacts = await getContacts(accountId);
|
final contacts = await getContacts(accountId, includeDeleted: true);
|
||||||
for (final c in contacts) {
|
for (final c in contacts) {
|
||||||
ContactCache.putPhone(c.id, c.phone);
|
ContactCache.putPhone(c.id, c.phone);
|
||||||
final fullName = (c.lastName != null && c.lastName!.isNotEmpty)
|
final fullName = (c.lastName != null && c.lastName!.isNotEmpty)
|
||||||
@@ -632,6 +650,7 @@ class ContactsModule {
|
|||||||
'base_raw_url': contact['baseRawUrl'] as String?,
|
'base_raw_url': contact['baseRawUrl'] as String?,
|
||||||
'update_time': (contact['updateTime'] as int?) ?? 0,
|
'update_time': (contact['updateTime'] as int?) ?? 0,
|
||||||
'options': optionsStr,
|
'options': optionsStr,
|
||||||
|
'account_status': (contact['accountStatus'] as int?) ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../config/app_phonebook_names.dart';
|
import '../config/app_phonebook_names.dart';
|
||||||
|
import '../utils/logger.dart';
|
||||||
|
|
||||||
class DeviceContactsService {
|
class DeviceContactsService {
|
||||||
DeviceContactsService._();
|
DeviceContactsService._();
|
||||||
|
|
||||||
static const _grantedKey = 'phonebook_granted';
|
|
||||||
static const _deniedKey = 'phonebook_denied';
|
static const _deniedKey = 'phonebook_denied';
|
||||||
|
|
||||||
static final Map<String, String> _byLast10 = {};
|
static final Map<String, String> _byLast10 = {};
|
||||||
@@ -16,6 +17,10 @@ class DeviceContactsService {
|
|||||||
|
|
||||||
static bool get _supported => Platform.isAndroid || Platform.isIOS;
|
static bool get _supported => Platform.isAndroid || Platform.isIOS;
|
||||||
|
|
||||||
|
static bool get isLoaded => _loaded;
|
||||||
|
|
||||||
|
static int get knownNumbers => _byLast10.length;
|
||||||
|
|
||||||
static String? _last10(String raw) {
|
static String? _last10(String raw) {
|
||||||
final digits = raw.replaceAll(RegExp(r'[^\d]'), '');
|
final digits = raw.replaceAll(RegExp(r'[^\d]'), '');
|
||||||
if (digits.length < 10) return null;
|
if (digits.length < 10) return null;
|
||||||
@@ -32,28 +37,43 @@ class DeviceContactsService {
|
|||||||
return name.trim();
|
return name.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<bool> hasPermission() async {
|
||||||
|
if (!_supported) return false;
|
||||||
|
try {
|
||||||
|
return await Permission.contacts.isGranted;
|
||||||
|
} catch (e) {
|
||||||
|
logger.w('Телефонная книга: не удалось прочитать статус разрешения: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> loadFromStartup() async {
|
static Future<void> loadFromStartup() async {
|
||||||
if (_loaded || !_supported) return;
|
if (_loaded || !_supported) return;
|
||||||
if (!AppPhonebookNames.current.value) return;
|
if (!AppPhonebookNames.current.value) return;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
if (!await hasPermission()) return;
|
||||||
if (prefs.getBool(_grantedKey) != true) return;
|
await _forgetDenial();
|
||||||
final granted = await FlutterContacts.requestPermission(readonly: true);
|
|
||||||
if (!granted) return;
|
|
||||||
await _readBook();
|
await _readBook();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> ensureLoadedInteractive({bool force = false}) async {
|
static Future<bool> ensureLoadedInteractive({bool force = false}) async {
|
||||||
if (_loaded || !_supported) return false;
|
if (!_supported) return false;
|
||||||
if (!AppPhonebookNames.current.value) return false;
|
if (!AppPhonebookNames.current.value) return false;
|
||||||
|
if (_loaded && !force) return false;
|
||||||
|
|
||||||
|
if (await hasPermission()) {
|
||||||
|
await _forgetDenial();
|
||||||
|
return _readBook();
|
||||||
|
}
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (!force && prefs.getBool(_deniedKey) == true) return false;
|
if (!force && prefs.getBool(_deniedKey) == true) return false;
|
||||||
|
|
||||||
final granted = await FlutterContacts.requestPermission(readonly: true);
|
final granted = await FlutterContacts.requestPermission(readonly: true);
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
await prefs.setBool(_deniedKey, true);
|
await prefs.setBool(_deniedKey, true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await prefs.remove(_deniedKey);
|
await prefs.remove(_deniedKey);
|
||||||
await prefs.setBool(_grantedKey, true);
|
|
||||||
return _readBook();
|
return _readBook();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +83,14 @@ class DeviceContactsService {
|
|||||||
return ensureLoadedInteractive(force: true);
|
return ensureLoadedInteractive(force: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _forgetDenial() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (prefs.getBool(_deniedKey) == true) await prefs.remove(_deniedKey);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<bool> _readBook() async {
|
static Future<bool> _readBook() async {
|
||||||
try {
|
try {
|
||||||
|
FlutterContacts.config.includeNonVisibleOnAndroid = true;
|
||||||
final contacts = await FlutterContacts.getContacts(withProperties: true);
|
final contacts = await FlutterContacts.getContacts(withProperties: true);
|
||||||
_byLast10.clear();
|
_byLast10.clear();
|
||||||
for (final contact in contacts) {
|
for (final contact in contacts) {
|
||||||
@@ -78,8 +104,13 @@ class DeviceContactsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
return _byLast10.isNotEmpty;
|
logger.i(
|
||||||
} catch (_) {
|
'Телефонная книга: прочитано ${contacts.length} записей, '
|
||||||
|
'${_byLast10.length} номеров',
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.w('Телефонная книга: не удалось прочитать: $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
Size videoNoteFrameSize(Size frame, double fallback) {
|
||||||
|
final width = frame.width.isFinite && frame.width > 0
|
||||||
|
? frame.width
|
||||||
|
: fallback;
|
||||||
|
final height = frame.height.isFinite && frame.height > 0
|
||||||
|
? frame.height
|
||||||
|
: fallback;
|
||||||
|
return Size(width, height);
|
||||||
|
}
|
||||||
@@ -221,7 +221,7 @@ class AppDatabase {
|
|||||||
await _migrateLegacyDb(target);
|
await _migrateLegacyDb(target);
|
||||||
return openDatabase(
|
return openDatabase(
|
||||||
target,
|
target,
|
||||||
version: 22,
|
version: 23,
|
||||||
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
||||||
onCreate: (db, _) => _createTables(db),
|
onCreate: (db, _) => _createTables(db),
|
||||||
onUpgrade: (db, oldVersion, newVersion) async {
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
@@ -353,6 +353,14 @@ class AppDatabase {
|
|||||||
await db.execute(_webAppStorageSchema);
|
await db.execute(_webAppStorageSchema);
|
||||||
await db.execute(_webAppBiometrySchema);
|
await db.execute(_webAppBiometrySchema);
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 23) {
|
||||||
|
await _addColumnIfMissing(
|
||||||
|
db,
|
||||||
|
'contacts',
|
||||||
|
'account_status',
|
||||||
|
'INTEGER NOT NULL DEFAULT 0',
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -465,7 +473,8 @@ class AppDatabase {
|
|||||||
base_url TEXT,
|
base_url TEXT,
|
||||||
base_raw_url TEXT,
|
base_raw_url TEXT,
|
||||||
update_time INTEGER NOT NULL DEFAULT 0,
|
update_time INTEGER NOT NULL DEFAULT 0,
|
||||||
options TEXT
|
options TEXT,
|
||||||
|
account_status INTEGER NOT NULL DEFAULT 0
|
||||||
)
|
)
|
||||||
''';
|
''';
|
||||||
|
|
||||||
@@ -798,6 +807,7 @@ class AppDatabase {
|
|||||||
static Future<void> close() async {
|
static Future<void> close() async {
|
||||||
await _db?.close();
|
await _db?.close();
|
||||||
_db = null;
|
_db = null;
|
||||||
|
_initCompleter = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chats cache
|
// Chats cache
|
||||||
@@ -1045,11 +1055,16 @@ class AppDatabase {
|
|||||||
await batch.commit(noResult: true);
|
await batch.commit(noResult: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<Map<String, dynamic>>> loadContacts(int accountId) async {
|
static Future<List<Map<String, dynamic>>> loadContacts(
|
||||||
|
int accountId, {
|
||||||
|
bool includeDeleted = false,
|
||||||
|
}) async {
|
||||||
final db = await _instance;
|
final db = await _instance;
|
||||||
return db.query(
|
return db.query(
|
||||||
'contacts',
|
'contacts',
|
||||||
where: 'account_id = ?',
|
where: includeDeleted
|
||||||
|
? 'account_id = ?'
|
||||||
|
: 'account_id = ? AND account_status = 0',
|
||||||
whereArgs: [accountId],
|
whereArgs: [accountId],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -863,6 +863,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
|||||||
final foldersKnown = await FoldersModule.hasReceivedFoldersList(p.id);
|
final foldersKnown = await FoldersModule.hasReceivedFoldersList(p.id);
|
||||||
final contactIds = (await ContactsModule.getContacts(
|
final contactIds = (await ContactsModule.getContacts(
|
||||||
p.id,
|
p.id,
|
||||||
|
includeDeleted: true,
|
||||||
)).map((c) => c.id).toSet();
|
)).map((c) => c.id).toSet();
|
||||||
|
|
||||||
const allChatsFolder = ChatFolder(
|
const allChatsFolder = ChatFolder(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:video_player/video_player.dart';
|
|||||||
import 'package:komet/main.dart';
|
import 'package:komet/main.dart';
|
||||||
|
|
||||||
import '../../../../core/media/media_playback.dart';
|
import '../../../../core/media/media_playback.dart';
|
||||||
|
import '../../../../core/media/video_note_frame.dart';
|
||||||
import '../../../../core/media/video_note_preloader.dart';
|
import '../../../../core/media/video_note_preloader.dart';
|
||||||
import '../../../../core/utils/format.dart';
|
import '../../../../core/utils/format.dart';
|
||||||
import '../../../../core/utils/haptics.dart';
|
import '../../../../core/utils/haptics.dart';
|
||||||
@@ -65,6 +66,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
|||||||
late final AnimationController _expand;
|
late final AnimationController _expand;
|
||||||
final ValueNotifier<double> _ringProgress = ValueNotifier(0);
|
final ValueNotifier<double> _ringProgress = ValueNotifier(0);
|
||||||
Uint8List? _preview;
|
Uint8List? _preview;
|
||||||
|
Size _frameSize = Size.zero;
|
||||||
VideoPlayerController? _controller;
|
VideoPlayerController? _controller;
|
||||||
VideoPlayerController? _local;
|
VideoPlayerController? _local;
|
||||||
Future<void>? _initializing;
|
Future<void>? _initializing;
|
||||||
@@ -195,8 +197,11 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
|||||||
? (value.position.inMilliseconds / total).clamp(0.0, 1.0)
|
? (value.position.inMilliseconds / total).clamp(0.0, 1.0)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
}
|
}
|
||||||
if (value.isPlaying != _playing) {
|
if (value.size != _frameSize || value.isPlaying != _playing) {
|
||||||
setState(() => _playing = value.isPlaying);
|
setState(() {
|
||||||
|
_frameSize = value.size;
|
||||||
|
_playing = value.isPlaying;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +244,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
|||||||
_controller = live;
|
_controller = live;
|
||||||
live.addListener(_onTick);
|
live.addListener(_onTick);
|
||||||
_PreviewPool.pin(this);
|
_PreviewPool.pin(this);
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() => _frameSize = live.value.size);
|
||||||
return live;
|
return live;
|
||||||
}
|
}
|
||||||
final running = _initializing;
|
final running = _initializing;
|
||||||
@@ -272,7 +277,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
|||||||
await controller.seekTo(Duration.zero);
|
await controller.seekTo(Duration.zero);
|
||||||
controller.addListener(_onTick);
|
controller.addListener(_onTick);
|
||||||
_PreviewPool.register(this);
|
_PreviewPool.register(this);
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() => _frameSize = controller.value.size);
|
||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +288,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
|||||||
_controller = null;
|
_controller = null;
|
||||||
controller.removeListener(_onTick);
|
controller.removeListener(_onTick);
|
||||||
MediaPlayback.instance.releaseVideoNote(controller);
|
MediaPlayback.instance.releaseVideoNote(controller);
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() => _frameSize = Size.zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _toggle() async {
|
Future<void> _toggle() async {
|
||||||
@@ -471,9 +476,14 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
|||||||
? _videoSurface(
|
? _videoSurface(
|
||||||
_controller!,
|
_controller!,
|
||||||
const ValueKey('note-video'),
|
const ValueKey('note-video'),
|
||||||
|
size,
|
||||||
)
|
)
|
||||||
: local != null && local.value.isInitialized
|
: local != null && local.value.isInitialized
|
||||||
? _videoSurface(local, const ValueKey('note-local'))
|
? _videoSurface(
|
||||||
|
local,
|
||||||
|
const ValueKey('note-local'),
|
||||||
|
size,
|
||||||
|
)
|
||||||
: _buildPoster(preview),
|
: _buildPoster(preview),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -559,15 +569,20 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _videoSurface(VideoPlayerController controller, Key key) {
|
Widget _videoSurface(
|
||||||
|
VideoPlayerController controller,
|
||||||
|
Key key,
|
||||||
|
double fallback,
|
||||||
|
) {
|
||||||
|
final frame = videoNoteFrameSize(controller.value.size, fallback);
|
||||||
return SizedBox.expand(
|
return SizedBox.expand(
|
||||||
key: key,
|
key: key,
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: controller.value.size.width,
|
width: frame.width,
|
||||||
height: controller.value.size.height,
|
height: frame.height,
|
||||||
child: VideoPlayer(controller),
|
child: VideoPlayer(controller),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
import '../../core/media/media_playback.dart';
|
import '../../core/media/media_playback.dart';
|
||||||
|
import '../../core/media/video_note_frame.dart';
|
||||||
import '../../core/utils/haptics.dart';
|
import '../../core/utils/haptics.dart';
|
||||||
import 'draggable_floating_layer.dart';
|
import 'draggable_floating_layer.dart';
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class _NoteCircle extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
final frame = track.controller.value.size;
|
final frame = videoNoteFrameSize(track.controller.value.size, size);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
@@ -81,8 +82,8 @@ class _NoteCircle extends StatelessWidget {
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: frame.width <= 0 ? size : frame.width,
|
width: frame.width,
|
||||||
height: frame.height <= 0 ? size : frame.height,
|
height: frame.height,
|
||||||
child: VideoPlayer(track.controller),
|
child: VideoPlayer(track.controller),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:komet/backend/modules/contacts.dart';
|
||||||
|
import 'package:komet/core/storage/app_database.dart';
|
||||||
|
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||||
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
|
|
||||||
|
class _SyntheticPathProvider extends PathProviderPlatform
|
||||||
|
with MockPlatformInterfaceMixin {
|
||||||
|
final String directory;
|
||||||
|
|
||||||
|
_SyntheticPathProvider(this.directory);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getApplicationSupportPath() async => directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<dynamic, dynamic> _serverContact({
|
||||||
|
required int id,
|
||||||
|
required String firstName,
|
||||||
|
required int phone,
|
||||||
|
int accountStatus = 0,
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'phone': phone,
|
||||||
|
'updateTime': 1,
|
||||||
|
'accountStatus': accountStatus,
|
||||||
|
'names': [
|
||||||
|
{'type': 'ONEME', 'firstName': firstName, 'lastName': 'Synthetic'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
const accountId = 1;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
final directory = Directory.systemTemp.createTempSync(
|
||||||
|
'synthetic_contacts_deleted_test',
|
||||||
|
);
|
||||||
|
PathProviderPlatform.instance = _SyntheticPathProvider(directory.path);
|
||||||
|
addTearDown(() async {
|
||||||
|
await AppDatabase.close();
|
||||||
|
if (directory.existsSync()) directory.deleteSync(recursive: true);
|
||||||
|
});
|
||||||
|
await AppDatabase.init();
|
||||||
|
await AppDatabase.saveProfile(
|
||||||
|
ProfileData(
|
||||||
|
id: accountId,
|
||||||
|
firstName: 'Synthetic owner',
|
||||||
|
phone: 100000,
|
||||||
|
country: 'ZZ',
|
||||||
|
accountStatus: 0,
|
||||||
|
updateTime: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await ContactsModule.syncFromLoginPayload({
|
||||||
|
'contacts': [
|
||||||
|
_serverContact(id: 11, firstName: 'Alive', phone: 700000011),
|
||||||
|
_serverContact(
|
||||||
|
id: 12,
|
||||||
|
firstName: 'Gone',
|
||||||
|
phone: 700000012,
|
||||||
|
accountStatus: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}, accountId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleted accounts are hidden from the contact list', () async {
|
||||||
|
final visible = await ContactsModule.getContacts(accountId);
|
||||||
|
|
||||||
|
expect(visible.map((c) => c.id), [11]);
|
||||||
|
expect(visible.single.isDeleted, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleted accounts stay available when explicitly requested', () async {
|
||||||
|
final all = await ContactsModule.getContacts(
|
||||||
|
accountId,
|
||||||
|
includeDeleted: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(all.map((c) => c.id).toSet(), {11, 12});
|
||||||
|
expect(all.firstWhere((c) => c.id == 12).isDeleted, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'a re-synced deleted account does not come back into the list',
|
||||||
|
() async {
|
||||||
|
await ContactsModule.syncFromLoginPayload({
|
||||||
|
'contacts': [
|
||||||
|
_serverContact(
|
||||||
|
id: 12,
|
||||||
|
firstName: 'Gone',
|
||||||
|
phone: 700000012,
|
||||||
|
accountStatus: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}, accountId);
|
||||||
|
|
||||||
|
final visible = await ContactsModule.getContacts(accountId);
|
||||||
|
|
||||||
|
expect(visible.map((c) => c.id), [11]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('a contact without accountStatus is treated as alive', () async {
|
||||||
|
await ContactsModule.syncFromLoginPayload({
|
||||||
|
'contacts': [
|
||||||
|
{
|
||||||
|
'id': 13,
|
||||||
|
'phone': 700000013,
|
||||||
|
'updateTime': 1,
|
||||||
|
'names': [
|
||||||
|
{'type': 'ONEME', 'firstName': 'Legacy', 'lastName': 'Synthetic'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, accountId);
|
||||||
|
|
||||||
|
final visible = await ContactsModule.getContacts(accountId);
|
||||||
|
|
||||||
|
expect(visible.map((c) => c.id).toSet(), {11, 13});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:komet/core/media/video_note_frame.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const fallback = 220.0;
|
||||||
|
|
||||||
|
test('a reported frame is used as is', () {
|
||||||
|
expect(
|
||||||
|
videoNoteFrameSize(const Size(480, 480), fallback),
|
||||||
|
const Size(480, 480),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
videoNoteFrameSize(const Size(640, 360), fallback),
|
||||||
|
const Size(640, 360),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an unreported frame falls back to a square of the circle size', () {
|
||||||
|
expect(videoNoteFrameSize(Size.zero, fallback), const Size(220, 220));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a half-reported frame keeps the dimension it does have', () {
|
||||||
|
expect(
|
||||||
|
videoNoteFrameSize(const Size(480, 0), fallback),
|
||||||
|
const Size(480, 220),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
videoNoteFrameSize(const Size(0, 480), fallback),
|
||||||
|
const Size(220, 480),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('negative and non-finite dimensions fall back', () {
|
||||||
|
expect(
|
||||||
|
videoNoteFrameSize(const Size(-1, -1), fallback),
|
||||||
|
const Size(220, 220),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
videoNoteFrameSize(const Size(double.nan, double.infinity), fallback),
|
||||||
|
const Size(220, 220),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user