fix: скрытие удаленных аккаунтов с контактов, возможно фикс скрытия медиа дорожки в кружках на кастомных android прошивках версии android ниже 16
This commit is contained in:
@@ -21,6 +21,7 @@ class CachedContact {
|
||||
final String? baseRawUrl;
|
||||
final int updateTime;
|
||||
final Set<String> options;
|
||||
final int accountStatus;
|
||||
|
||||
const CachedContact({
|
||||
required this.id,
|
||||
@@ -33,12 +34,14 @@ class CachedContact {
|
||||
this.baseRawUrl,
|
||||
required this.updateTime,
|
||||
this.options = const {},
|
||||
this.accountStatus = 0,
|
||||
});
|
||||
|
||||
bool get isOfficial => options.contains('OFFICIAL');
|
||||
bool get isBot => options.contains('BOT');
|
||||
bool get isServiceAccount => options.contains('SERVICE_ACCOUNT');
|
||||
bool get isVerified => isOfficial;
|
||||
bool get isDeleted => accountStatus != 0;
|
||||
|
||||
factory CachedContact.fromDbRow(Map<String, dynamic> row) => CachedContact(
|
||||
id: row['id'] as int,
|
||||
@@ -51,6 +54,7 @@ class CachedContact {
|
||||
baseRawUrl: row['base_raw_url'] as String?,
|
||||
updateTime: row['update_time'] as int,
|
||||
options: _decodeOptions(row['options']),
|
||||
accountStatus: (row['account_status'] as int?) ?? 0,
|
||||
);
|
||||
|
||||
static Set<String> _decodeOptions(dynamic raw) {
|
||||
@@ -183,6 +187,7 @@ class ContactsModule {
|
||||
'base_raw_url': null,
|
||||
'update_time': 0,
|
||||
'options': null,
|
||||
'account_status': 0,
|
||||
};
|
||||
|
||||
if (row == null) return null;
|
||||
@@ -397,7 +402,14 @@ class ContactsModule {
|
||||
int accountId,
|
||||
) async {
|
||||
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>>[];
|
||||
for (final raw in contacts.whereType<Map>()) {
|
||||
@@ -492,8 +504,14 @@ class ContactsModule {
|
||||
return photos;
|
||||
}
|
||||
|
||||
static Future<List<CachedContact>> getContacts(int accountId) async {
|
||||
final rows = await AppDatabase.loadContacts(accountId);
|
||||
static Future<List<CachedContact>> getContacts(
|
||||
int accountId, {
|
||||
bool includeDeleted = false,
|
||||
}) async {
|
||||
final rows = await AppDatabase.loadContacts(
|
||||
accountId,
|
||||
includeDeleted: includeDeleted,
|
||||
);
|
||||
return rows.map(CachedContact.fromDbRow).toList();
|
||||
}
|
||||
|
||||
@@ -571,7 +589,7 @@ class ContactsModule {
|
||||
/// Прогревает in-memory ContactCache из локальных контактов.
|
||||
/// Нужно вызывать на cold start: иначе кэш пуст до следующего логина.
|
||||
static Future<void> primeCacheFromDb(int accountId) async {
|
||||
final contacts = await getContacts(accountId);
|
||||
final contacts = await getContacts(accountId, includeDeleted: true);
|
||||
for (final c in contacts) {
|
||||
ContactCache.putPhone(c.id, c.phone);
|
||||
final fullName = (c.lastName != null && c.lastName!.isNotEmpty)
|
||||
@@ -632,6 +650,7 @@ class ContactsModule {
|
||||
'base_raw_url': contact['baseRawUrl'] as String?,
|
||||
'update_time': (contact['updateTime'] as int?) ?? 0,
|
||||
'options': optionsStr,
|
||||
'account_status': (contact['accountStatus'] as int?) ?? 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../config/app_phonebook_names.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
class DeviceContactsService {
|
||||
DeviceContactsService._();
|
||||
|
||||
static const _grantedKey = 'phonebook_granted';
|
||||
static const _deniedKey = 'phonebook_denied';
|
||||
|
||||
static final Map<String, String> _byLast10 = {};
|
||||
@@ -16,6 +17,10 @@ class DeviceContactsService {
|
||||
|
||||
static bool get _supported => Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
static bool get isLoaded => _loaded;
|
||||
|
||||
static int get knownNumbers => _byLast10.length;
|
||||
|
||||
static String? _last10(String raw) {
|
||||
final digits = raw.replaceAll(RegExp(r'[^\d]'), '');
|
||||
if (digits.length < 10) return null;
|
||||
@@ -32,28 +37,43 @@ class DeviceContactsService {
|
||||
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 {
|
||||
if (_loaded || !_supported) return;
|
||||
if (!AppPhonebookNames.current.value) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool(_grantedKey) != true) return;
|
||||
final granted = await FlutterContacts.requestPermission(readonly: true);
|
||||
if (!granted) return;
|
||||
if (!await hasPermission()) return;
|
||||
await _forgetDenial();
|
||||
await _readBook();
|
||||
}
|
||||
|
||||
static Future<bool> ensureLoadedInteractive({bool force = false}) async {
|
||||
if (_loaded || !_supported) return false;
|
||||
if (!_supported) 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();
|
||||
if (!force && prefs.getBool(_deniedKey) == true) return false;
|
||||
|
||||
final granted = await FlutterContacts.requestPermission(readonly: true);
|
||||
if (!granted) {
|
||||
await prefs.setBool(_deniedKey, true);
|
||||
return false;
|
||||
}
|
||||
await prefs.remove(_deniedKey);
|
||||
await prefs.setBool(_grantedKey, true);
|
||||
return _readBook();
|
||||
}
|
||||
|
||||
@@ -63,8 +83,14 @@ class DeviceContactsService {
|
||||
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 {
|
||||
try {
|
||||
FlutterContacts.config.includeNonVisibleOnAndroid = true;
|
||||
final contacts = await FlutterContacts.getContacts(withProperties: true);
|
||||
_byLast10.clear();
|
||||
for (final contact in contacts) {
|
||||
@@ -78,8 +104,13 @@ class DeviceContactsService {
|
||||
}
|
||||
}
|
||||
_loaded = true;
|
||||
return _byLast10.isNotEmpty;
|
||||
} catch (_) {
|
||||
logger.i(
|
||||
'Телефонная книга: прочитано ${contacts.length} записей, '
|
||||
'${_byLast10.length} номеров',
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.w('Телефонная книга: не удалось прочитать: $e');
|
||||
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);
|
||||
return openDatabase(
|
||||
target,
|
||||
version: 22,
|
||||
version: 23,
|
||||
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
||||
onCreate: (db, _) => _createTables(db),
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
@@ -353,6 +353,14 @@ class AppDatabase {
|
||||
await db.execute(_webAppStorageSchema);
|
||||
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_raw_url TEXT,
|
||||
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 {
|
||||
await _db?.close();
|
||||
_db = null;
|
||||
_initCompleter = null;
|
||||
}
|
||||
|
||||
// Chats cache
|
||||
@@ -1045,11 +1055,16 @@ class AppDatabase {
|
||||
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;
|
||||
return db.query(
|
||||
'contacts',
|
||||
where: 'account_id = ?',
|
||||
where: includeDeleted
|
||||
? 'account_id = ?'
|
||||
: 'account_id = ? AND account_status = 0',
|
||||
whereArgs: [accountId],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -863,6 +863,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
final foldersKnown = await FoldersModule.hasReceivedFoldersList(p.id);
|
||||
final contactIds = (await ContactsModule.getContacts(
|
||||
p.id,
|
||||
includeDeleted: true,
|
||||
)).map((c) => c.id).toSet();
|
||||
|
||||
const allChatsFolder = ChatFolder(
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:video_player/video_player.dart';
|
||||
import 'package:komet/main.dart';
|
||||
|
||||
import '../../../../core/media/media_playback.dart';
|
||||
import '../../../../core/media/video_note_frame.dart';
|
||||
import '../../../../core/media/video_note_preloader.dart';
|
||||
import '../../../../core/utils/format.dart';
|
||||
import '../../../../core/utils/haptics.dart';
|
||||
@@ -65,6 +66,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
||||
late final AnimationController _expand;
|
||||
final ValueNotifier<double> _ringProgress = ValueNotifier(0);
|
||||
Uint8List? _preview;
|
||||
Size _frameSize = Size.zero;
|
||||
VideoPlayerController? _controller;
|
||||
VideoPlayerController? _local;
|
||||
Future<void>? _initializing;
|
||||
@@ -195,8 +197,11 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
||||
? (value.position.inMilliseconds / total).clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
}
|
||||
if (value.isPlaying != _playing) {
|
||||
setState(() => _playing = value.isPlaying);
|
||||
if (value.size != _frameSize || value.isPlaying != _playing) {
|
||||
setState(() {
|
||||
_frameSize = value.size;
|
||||
_playing = value.isPlaying;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +244,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
||||
_controller = live;
|
||||
live.addListener(_onTick);
|
||||
_PreviewPool.pin(this);
|
||||
if (mounted) setState(() {});
|
||||
if (mounted) setState(() => _frameSize = live.value.size);
|
||||
return live;
|
||||
}
|
||||
final running = _initializing;
|
||||
@@ -272,7 +277,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
||||
await controller.seekTo(Duration.zero);
|
||||
controller.addListener(_onTick);
|
||||
_PreviewPool.register(this);
|
||||
if (mounted) setState(() {});
|
||||
if (mounted) setState(() => _frameSize = controller.value.size);
|
||||
return controller;
|
||||
}
|
||||
|
||||
@@ -283,7 +288,7 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
||||
_controller = null;
|
||||
controller.removeListener(_onTick);
|
||||
MediaPlayback.instance.releaseVideoNote(controller);
|
||||
if (mounted) setState(() {});
|
||||
if (mounted) setState(() => _frameSize = Size.zero);
|
||||
}
|
||||
|
||||
Future<void> _toggle() async {
|
||||
@@ -471,9 +476,14 @@ class _VideoNoteBubbleState extends State<VideoNoteBubble>
|
||||
? _videoSurface(
|
||||
_controller!,
|
||||
const ValueKey('note-video'),
|
||||
size,
|
||||
)
|
||||
: local != null && local.value.isInitialized
|
||||
? _videoSurface(local, const ValueKey('note-local'))
|
||||
? _videoSurface(
|
||||
local,
|
||||
const ValueKey('note-local'),
|
||||
size,
|
||||
)
|
||||
: _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(
|
||||
key: key,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: SizedBox(
|
||||
width: controller.value.size.width,
|
||||
height: controller.value.size.height,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
child: VideoPlayer(controller),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../core/media/media_playback.dart';
|
||||
import '../../core/media/video_note_frame.dart';
|
||||
import '../../core/utils/haptics.dart';
|
||||
import 'draggable_floating_layer.dart';
|
||||
|
||||
@@ -65,7 +66,7 @@ class _NoteCircle extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final frame = track.controller.value.size;
|
||||
final frame = videoNoteFrameSize(track.controller.value.size, size);
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
@@ -81,8 +82,8 @@ class _NoteCircle extends StatelessWidget {
|
||||
fit: BoxFit.cover,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: SizedBox(
|
||||
width: frame.width <= 0 ? size : frame.width,
|
||||
height: frame.height <= 0 ? size : frame.height,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
child: VideoPlayer(track.controller),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user