feat: glossy стиль, возможность выбора frost blur. fix: переход к закрепленному сообщению

This commit is contained in:
Jganenokk
2026-07-13 19:12:51 +07:00
parent 10d071bdff
commit c527620a15
27 changed files with 893 additions and 632 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ class AppChatChrome {
static final _setting = PersistedEnum<ChatChromeStyle>( static final _setting = PersistedEnum<ChatChromeStyle>(
prefKey: prefKey, prefKey: prefKey,
defaultValue: ChatChromeStyle.none, defaultValue: ChatChromeStyle.transparent,
encode: _encode, encode: _encode,
decode: _parse, decode: _parse,
); );
@@ -17,7 +17,7 @@ class AppChatChrome {
static ValueNotifier<ChatChromeStyle> get current => _setting.current; static ValueNotifier<ChatChromeStyle> get current => _setting.current;
static ChatChromeStyle _parse(String? value) => static ChatChromeStyle _parse(String? value) =>
enumFromName(ChatChromeStyle.values, value, ChatChromeStyle.none); enumFromName(ChatChromeStyle.values, value, ChatChromeStyle.transparent);
static String _encode(ChatChromeStyle value) => value.name; static String _encode(ChatChromeStyle value) => value.name;
@@ -0,0 +1,25 @@
import 'package:flutter/foundation.dart';
import 'persisted_setting.dart';
enum ComposerBackground { standard, frostBlur }
class AppComposerBackground {
static const prefKey = 'app_composer_background';
static final _setting = PersistedEnum<ComposerBackground>(
prefKey: prefKey,
defaultValue: ComposerBackground.standard,
encode: (value) => value.name,
decode: _parse,
);
static ValueNotifier<ComposerBackground> get current => _setting.current;
static Future<ComposerBackground> load() => _setting.load();
static Future<void> save(ComposerBackground value) => _setting.save(value);
static ComposerBackground _parse(String? val) =>
enumFromName(ComposerBackground.values, val, ComposerBackground.standard);
}
+25
View File
@@ -0,0 +1,25 @@
import 'package:flutter/foundation.dart';
import 'persisted_setting.dart';
enum ComposerStyle { glossy, materialYou }
class AppComposerStyle {
static const prefKey = 'app_composer_style';
static final _setting = PersistedEnum<ComposerStyle>(
prefKey: prefKey,
defaultValue: ComposerStyle.glossy,
encode: (value) => value.name,
decode: _parse,
);
static ValueNotifier<ComposerStyle> get current => _setting.current;
static Future<ComposerStyle> load() => _setting.load();
static Future<void> save(ComposerStyle value) => _setting.save(value);
static ComposerStyle _parse(String? val) =>
enumFromName(ComposerStyle.values, val, ComposerStyle.glossy);
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class AppFrost {
static const double sigma = 34;
static const double panelSigma = 24;
static Color panelTint(ColorScheme cs) => cs.surface.withValues(alpha: 0.38);
static Color blurPanelTint(ColorScheme cs) =>
cs.surfaceContainerHigh.withValues(alpha: 0.55);
static Color pillTint(ColorScheme cs) =>
cs.surfaceContainerHigh.withValues(alpha: 0.45);
static Color inputTint(ColorScheme cs) =>
cs.surfaceContainerHighest.withValues(alpha: 0.45);
static BorderSide hairline(ColorScheme cs) =>
BorderSide(color: cs.outlineVariant.withValues(alpha: 0.4), width: 0.5);
}
@@ -9,7 +9,7 @@ class AppMessageActionsStyle {
static final _setting = PersistedEnum<MessageActionsStyle>( static final _setting = PersistedEnum<MessageActionsStyle>(
prefKey: prefKey, prefKey: prefKey,
defaultValue: MessageActionsStyle.radial, defaultValue: MessageActionsStyle.list,
encode: (value) => value.name, encode: (value) => value.name,
decode: _parse, decode: _parse,
); );
@@ -21,7 +21,7 @@ class AppMessageActionsStyle {
static Future<void> save(MessageActionsStyle style) => _setting.save(style); static Future<void> save(MessageActionsStyle style) => _setting.save(style);
static MessageActionsStyle _parse(String? val) => static MessageActionsStyle _parse(String? val) =>
enumFromName(MessageActionsStyle.values, val, MessageActionsStyle.radial); enumFromName(MessageActionsStyle.values, val, MessageActionsStyle.list);
static String label(MessageActionsStyle style) { static String label(MessageActionsStyle style) {
switch (style) { switch (style) {
+1 -1
View File
@@ -7,7 +7,7 @@ class AppPillGradient {
static final _setting = PersistedSetting<bool>( static final _setting = PersistedSetting<bool>(
prefKey: prefKey, prefKey: prefKey,
defaultValue: true, defaultValue: false,
read: (prefs, key) => prefs.getBool(key), read: (prefs, key) => prefs.getBool(key),
write: (prefs, key, value) async { write: (prefs, key, value) async {
await prefs.setBool(key, value); await prefs.setBool(key, value);
+2 -2
View File
@@ -9,7 +9,7 @@ class AppVisualStyle {
static final _setting = PersistedEnum<VisualStyle>( static final _setting = PersistedEnum<VisualStyle>(
prefKey: prefKey, prefKey: prefKey,
defaultValue: VisualStyle.materialYou, defaultValue: VisualStyle.glossy,
encode: _encode, encode: _encode,
decode: _parse, decode: _parse,
); );
@@ -23,5 +23,5 @@ class AppVisualStyle {
static String _encode(VisualStyle value) => value.name; static String _encode(VisualStyle value) => value.name;
static VisualStyle _parse(String? val) => static VisualStyle _parse(String? val) =>
enumFromName(VisualStyle.values, val, VisualStyle.materialYou); enumFromName(VisualStyle.values, val, VisualStyle.glossy);
} }
+50
View File
@@ -928,6 +928,56 @@ class AppDatabase {
); );
} }
static Future<List<Map<String, dynamic>>> loadMessagesBetween(
int accountId,
int chatId, {
required int afterTime,
required int beforeTime,
int limit = 60,
bool onlyVisible = false,
}) async {
final db = await _instance;
return db.query(
'messages',
where: onlyVisible
? 'account_id = ? AND chat_id = ? AND deleted = 0 '
'AND time > ? AND time < ?'
: 'account_id = ? AND chat_id = ? AND time > ? AND time < ?',
whereArgs: [accountId, chatId, afterTime, beforeTime],
orderBy: 'time ASC',
limit: limit,
);
}
static Future<List<Map<String, dynamic>>> loadMessagesAround(
int accountId,
int chatId, {
required int centerTime,
int before = 40,
int after = 20,
bool onlyVisible = false,
}) async {
final db = await _instance;
final base = onlyVisible
? 'account_id = ? AND chat_id = ? AND deleted = 0'
: 'account_id = ? AND chat_id = ?';
final older = await db.query(
'messages',
where: '$base AND time <= ?',
whereArgs: [accountId, chatId, centerTime],
orderBy: 'time DESC',
limit: before,
);
final newer = await db.query(
'messages',
where: '$base AND time > ?',
whereArgs: [accountId, chatId, centerTime],
orderBy: 'time ASC',
limit: after,
);
return [...newer.reversed, ...older];
}
static Future<void> markMessageDeleted( static Future<void> markMessageDeleted(
int accountId, int accountId,
int chatId, int chatId,
@@ -10,9 +10,25 @@ import '../../../../core/storage/app_database.dart';
import '../../../../core/utils/logger.dart'; import '../../../../core/utils/logger.dart';
import '../../../../main.dart'; import '../../../../main.dart';
class HistoryGap {
HistoryGap({
required this.edgeId,
required this.edgeTime,
required this.tailTime,
});
String edgeId;
int edgeTime;
final int tailTime;
}
class ChatController extends ChangeNotifier { class ChatController extends ChangeNotifier {
static const int historyPageSize = 30; static const int historyPageSize = 30;
static const int historyInitialLimit = 50; static const int historyInitialLimit = 50;
static const int jumpWindowBefore = 40;
static const int jumpWindowAfter = 20;
static const int historyWalkPageSize = 200;
static const int gapPageSize = 60;
int chatId = 0; int chatId = 0;
int myId = 0; int myId = 0;
@@ -23,6 +39,11 @@ class ChatController extends ChangeNotifier {
bool hasMoreHistory = true; bool hasMoreHistory = true;
bool isLoadingMore = false; bool isLoadingMore = false;
bool historyKickedOff = false; bool historyKickedOff = false;
bool loadingGap = false;
final List<HistoryGap> gaps = [];
bool get hasGap => gaps.isNotEmpty;
bool Function() isMounted = () => true; bool Function() isMounted = () => true;
@@ -93,20 +114,179 @@ class ChatController extends ChangeNotifier {
Future<List<CachedMessage>> loadOlderFromDb( Future<List<CachedMessage>> loadOlderFromDb(
int beforeTime, int beforeTime,
bool onlyVisible, bool onlyVisible, {
) async { int? limit,
}) async {
final rows = await AppDatabase.loadMessagesBefore( final rows = await AppDatabase.loadMessagesBefore(
myId, myId,
chatId, chatId,
beforeTime: beforeTime, beforeTime: beforeTime,
limit: historyPageSize, limit: limit ?? historyPageSize,
onlyVisible: onlyVisible, onlyVisible: onlyVisible,
); );
return CachedMessage.fromDbRowsAsync(rows); return CachedMessage.fromDbRowsAsync(rows);
} }
Future<List<CachedMessage>> loadGapSliceFromDb(
int afterTime,
int beforeTime,
bool onlyVisible,
) async {
final rows = await AppDatabase.loadMessagesBetween(
myId,
chatId,
afterTime: afterTime,
beforeTime: beforeTime,
limit: gapPageSize,
onlyVisible: onlyVisible,
);
return CachedMessage.fromDbRowsAsync(rows);
}
Future<List<CachedMessage>> loadWindowFromDb(
int centerTime,
bool onlyVisible,
) async {
final rows = await AppDatabase.loadMessagesAround(
myId,
chatId,
centerTime: centerTime,
before: jumpWindowBefore,
after: jumpWindowAfter,
onlyVisible: onlyVisible,
);
return CachedMessage.fromDbRowsAsync(rows);
}
Future<bool> loadMessageWindow({
required String targetId,
required int targetTime,
}) async {
if (myId == 0 || targetTime <= 0) return false;
final onlyVisible = !KometSettings.viewDeleted.value;
var window = await loadWindowFromDb(targetTime, onlyVisible);
if (!isMounted()) return false;
if (!window.any((m) => m.id == targetId)) {
final fetched = await messagesModule.fetchHistory(
myId,
chatId,
fromTime: targetTime + 1,
forward: jumpWindowAfter,
backward: jumpWindowBefore + 1,
);
if (!isMounted()) return false;
if (fetched.isNotEmpty && KometSettings.viewDeleted.value) {
await chats.reconcileDeletedFromFetch(myId, chatId, fetched);
}
window = await loadWindowFromDb(targetTime, onlyVisible);
if (!isMounted()) return false;
}
if (window.isEmpty) return false;
final oldestLoaded = messages.isEmpty ? 0 : messages.first.time;
final reachesLoaded =
messages.isEmpty || window.any((m) => m.time >= oldestLoaded);
mergeMessages(window);
if (reachesLoaded) {
persistSessionCache();
} else {
_markGapAfterWindow(window);
}
return messages.any((m) => m.id == targetId);
}
void _markGapAfterWindow(List<CachedMessage> window) {
var edge = window.first;
for (final m in window) {
if (m.time > edge.time) edge = m;
}
final idx = messages.indexWhere((m) => m.id == edge.id);
if (idx == -1 || idx + 1 >= messages.length) return;
final tailTime = messages[idx + 1].time;
gaps.removeWhere((g) => g.tailTime == tailTime);
gaps.add(
HistoryGap(edgeId: edge.id, edgeTime: edge.time, tailTime: tailTime),
);
}
void _closeGap(HistoryGap gap) {
gaps.remove(gap);
if (gaps.isEmpty) persistSessionCache();
}
Future<int> fillGapForward(HistoryGap gap) async {
if (loadingGap || myId == 0 || !gaps.contains(gap)) return 0;
if (gap.edgeTime <= 0 || gap.tailTime <= gap.edgeTime) {
_closeGap(gap);
return 0;
}
loadingGap = true;
try {
final onlyVisible = !KometSettings.viewDeleted.value;
var slice = await loadGapSliceFromDb(
gap.edgeTime,
gap.tailTime,
onlyVisible,
);
if (!isMounted()) return 0;
if (slice.length < gapPageSize) {
final fetched = await messagesModule.fetchHistory(
myId,
chatId,
fromTime: gap.edgeTime,
forward: gapPageSize,
backward: 0,
);
if (!isMounted()) return 0;
if (fetched.isNotEmpty && KometSettings.viewDeleted.value) {
await chats.reconcileDeletedFromFetch(myId, chatId, fetched);
}
final refreshed = await loadGapSliceFromDb(
gap.edgeTime,
gap.tailTime,
onlyVisible,
);
if (!isMounted()) return 0;
if (refreshed.length <= slice.length) {
if (refreshed.isNotEmpty) mergeMessages(refreshed);
_closeGap(gap);
return refreshed.length;
}
slice = refreshed;
}
if (slice.isEmpty) {
_closeGap(gap);
return 0;
}
mergeMessages(slice);
var edge = slice.first;
for (final m in slice) {
if (m.time > edge.time) edge = m;
}
gap.edgeId = edge.id;
gap.edgeTime = edge.time;
if (edge.time >= gap.tailTime) _closeGap(gap);
return slice.length;
} catch (e) {
logger.e('Error filling history gap: $e');
return 0;
} finally {
loadingGap = false;
}
}
void persistSessionCache() { void persistSessionCache() {
if (myId == 0 || messages.isEmpty) return; if (myId == 0 || messages.isEmpty || hasGap) return;
MessageSessionCache.save( MessageSessionCache.save(
myId, myId,
chatId, chatId,
@@ -119,29 +299,32 @@ class ChatController extends ChangeNotifier {
required void Function() onLoadingStarted, required void Function() onLoadingStarted,
required void Function(int added) onLoaded, required void Function(int added) onLoaded,
required void Function(Object error) onError, required void Function(Object error) onError,
int? pageSize,
bool persist = true,
}) async { }) async {
if (isLoadingMore || !hasMoreHistory || messages.isEmpty) return; if (isLoadingMore || !hasMoreHistory || messages.isEmpty) return;
isLoadingMore = true; isLoadingMore = true;
onLoadingStarted(); onLoadingStarted();
final size = pageSize ?? historyPageSize;
final oldest = messages.first; final oldest = messages.first;
final onlyVisible = !KometSettings.viewDeleted.value; final onlyVisible = !KometSettings.viewDeleted.value;
try { try {
var older = await loadOlderFromDb(oldest.time, onlyVisible); var older = await loadOlderFromDb(oldest.time, onlyVisible, limit: size);
if (older.length < historyPageSize) { if (older.length < size) {
final fetched = await messagesModule.fetchHistory( final fetched = await messagesModule.fetchHistory(
myId, myId,
chatId, chatId,
fromTime: oldest.time, fromTime: oldest.time,
count: historyPageSize, count: size,
); );
if (fetched.isNotEmpty) { if (fetched.isNotEmpty) {
if (KometSettings.viewDeleted.value) { if (KometSettings.viewDeleted.value) {
await chats.reconcileDeletedFromFetch(myId, chatId, fetched); await chats.reconcileDeletedFromFetch(myId, chatId, fetched);
} }
older = await loadOlderFromDb(oldest.time, onlyVisible); older = await loadOlderFromDb(oldest.time, onlyVisible, limit: size);
} }
} }
@@ -149,7 +332,7 @@ class ChatController extends ChangeNotifier {
final added = prependOlder(older); final added = prependOlder(older);
isLoadingMore = false; isLoadingMore = false;
if (added == 0) hasMoreHistory = false; if (added == 0) hasMoreHistory = false;
persistSessionCache(); if (persist) persistSessionCache();
onLoaded(added); onLoaded(added);
} catch (e) { } catch (e) {
logger.e('Error loading more history: $e'); logger.e('Error loading more history: $e');
@@ -2,11 +2,14 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/core/config/app_frost.dart';
import 'package:komet/frontend/widgets/glossy_pill.dart'; import 'package:komet/frontend/widgets/glossy_pill.dart';
import 'package:komet/frontend/widgets/online_dot.dart'; import 'package:komet/frontend/widgets/online_dot.dart';
class ChatHeaderRow extends StatelessWidget { class ChatHeaderRow extends StatelessWidget {
final bool glossy; final bool glossy;
final bool frosted;
final BackdropKey? backdropKey;
final ColorScheme cs; final ColorScheme cs;
final bool embedded; final bool embedded;
final int chatId; final int chatId;
@@ -28,6 +31,8 @@ class ChatHeaderRow extends StatelessWidget {
const ChatHeaderRow({ const ChatHeaderRow({
super.key, super.key,
required this.glossy, required this.glossy,
required this.frosted,
this.backdropKey,
required this.cs, required this.cs,
required this.embedded, required this.embedded,
required this.chatId, required this.chatId,
@@ -51,6 +56,10 @@ class ChatHeaderRow extends StatelessWidget {
Widget build(BuildContext context) => Widget build(BuildContext context) =>
glossy ? _glossyRow(context) : _materialRow(context); glossy ? _glossyRow(context) : _materialRow(context);
Color? get _pillColor => frosted ? AppFrost.pillTint(cs) : null;
double? get _pillBlur => frosted ? AppFrost.sigma : null;
Widget _glossyRow(BuildContext context) { Widget _glossyRow(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(10, 4, 10, 8), padding: const EdgeInsets.fromLTRB(10, 4, 10, 8),
@@ -62,6 +71,9 @@ class ChatHeaderRow extends StatelessWidget {
width: 56, width: 56,
height: 56, height: 56,
child: GlossyPill( child: GlossyPill(
color: _pillColor,
blurSigma: _pillBlur,
backdropKey: backdropKey,
onTap: () { onTap: () {
if (embedded) { if (embedded) {
onClose?.call(); onClose?.call();
@@ -83,6 +95,9 @@ class ChatHeaderRow extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: GlossyPill( child: GlossyPill(
color: _pillColor,
blurSigma: _pillBlur,
backdropKey: backdropKey,
onTap: onOpenInfo, onTap: onOpenInfo,
padding: const EdgeInsets.fromLTRB(6, 6, 16, 6), padding: const EdgeInsets.fromLTRB(6, 6, 16, 6),
child: Row( child: Row(
@@ -168,6 +183,9 @@ class ChatHeaderRow extends StatelessWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
GlossyPill( GlossyPill(
color: _pillColor,
blurSigma: _pillBlur,
backdropKey: backdropKey,
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),
child: SizedBox( child: SizedBox(
height: 56, height: 56,
@@ -8,6 +8,9 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/backend/modules/messages.dart'; import 'package:komet/backend/modules/messages.dart';
import 'package:komet/core/config/app_chat_chrome.dart'; import 'package:komet/core/config/app_chat_chrome.dart';
import 'package:komet/core/config/app_colors.dart'; import 'package:komet/core/config/app_colors.dart';
import 'package:komet/core/config/app_composer_background.dart';
import 'package:komet/core/config/app_composer_style.dart';
import 'package:komet/core/config/app_frost.dart';
import 'package:komet/frontend/screens/chats/chat/upload_status.dart'; import 'package:komet/frontend/screens/chats/chat/upload_status.dart';
import 'package:komet/frontend/screens/chats/chat/video_note_controller.dart'; import 'package:komet/frontend/screens/chats/chat/video_note_controller.dart';
import 'package:komet/frontend/screens/chats/chat/voice_record_controller.dart'; import 'package:komet/frontend/screens/chats/chat/voice_record_controller.dart';
@@ -19,6 +22,9 @@ class ComposerInputBar extends StatelessWidget {
super.key, super.key,
required this.chatType, required this.chatType,
required this.chrome, required this.chrome,
required this.style,
required this.background,
this.backdropKey,
required this.attachAnim, required this.attachAnim,
required this.replyTo, required this.replyTo,
required this.myId, required this.myId,
@@ -43,6 +49,9 @@ class ComposerInputBar extends StatelessWidget {
final String chatType; final String chatType;
final ChatChromeStyle chrome; final ChatChromeStyle chrome;
final ComposerStyle style;
final ComposerBackground background;
final BackdropKey? backdropKey;
final Animation<double> attachAnim; final Animation<double> attachAnim;
final ValueListenable<CachedMessage?> replyTo; final ValueListenable<CachedMessage?> replyTo;
final int myId; final int myId;
@@ -104,7 +113,7 @@ class ComposerInputBar extends StatelessWidget {
); );
} }
return SafeArea( final bar = SafeArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -124,18 +133,9 @@ class ComposerInputBar extends StatelessWidget {
minHeight: 54, minHeight: 54,
maxHeight: 180, maxHeight: 180,
), ),
child: GlossyPill( child: _fieldSurface(
color: Color.alphaBlend( cs,
cs.surfaceContainerHighest.withValues(alpha: 0.92), Stack(
cs.surface,
),
borderRadius: BorderRadius.circular(28),
depth: 8,
borderSide: BorderSide(
color: cs.outlineVariant.withValues(alpha: 0.5),
width: 0.5,
),
child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
AnimatedBuilder( AnimatedBuilder(
@@ -317,14 +317,16 @@ class ComposerInputBar extends StatelessWidget {
builder: (context, videoMode, _) { builder: (context, videoMode, _) {
final sendMode = final sendMode =
hasText || locked; hasText || locked;
final pill = GlossyPill( final pill = _actionSurface(
color: sendMode color: _flat
? Colors.transparent
: sendMode
? cs.primary ? cs.primary
: recording : recording
? cs.error ? cs.error
: _frost
? AppFrost.inputTint(cs)
: cs.surfaceContainerHighest, : cs.surfaceContainerHighest,
borderRadius:
BorderRadius.circular(27),
onTap: hasText onTap: hasText
? onSendText ? onSendText
: locked : locked
@@ -335,7 +337,6 @@ class ComposerInputBar extends StatelessWidget {
onLongPress: hasText onLongPress: hasText
? onScheduleMessage ? onScheduleMessage
: null, : null,
depth: 8,
child: SizedBox( child: SizedBox(
width: 54, width: 54,
height: 54, height: 54,
@@ -346,7 +347,13 @@ class ComposerInputBar extends StatelessWidget {
: videoMode : videoMode
? Symbols.videocam ? Symbols.videocam
: Symbols.mic, : Symbols.mic,
color: sendMode color: _flat
? (sendMode
? cs.primary
: recording
? cs.error
: cs.onSurfaceVariant)
: sendMode
? cs.onPrimary ? cs.onPrimary
: recording : recording
? cs.onError ? cs.onError
@@ -405,6 +412,72 @@ class ComposerInputBar extends StatelessWidget {
], ],
), ),
); );
return _barSurface(cs, bar);
}
bool get _flat => style == ComposerStyle.materialYou;
bool get _frost => background == ComposerBackground.frostBlur;
Widget _barSurface(ColorScheme cs, Widget child) {
if (!_flat || background == ComposerBackground.frostBlur) return child;
return DecoratedBox(
decoration: BoxDecoration(
color: cs.surface,
border: Border(top: AppFrost.hairline(cs)),
),
child: child,
);
}
Widget _fieldSurface(ColorScheme cs, Widget child) {
if (_flat) return child;
return GlossyPill(
color: _frost
? AppFrost.inputTint(cs)
: Color.alphaBlend(
cs.surfaceContainerHighest.withValues(alpha: 0.92),
cs.surface,
),
blurSigma: _frost ? AppFrost.sigma : null,
backdropKey: backdropKey,
borderRadius: BorderRadius.circular(28),
depth: 8,
borderSide: BorderSide(
color: cs.outlineVariant.withValues(alpha: 0.5),
width: 0.5,
),
child: child,
);
}
Widget _actionSurface({
required Color color,
required Widget child,
VoidCallback? onTap,
VoidCallback? onLongPress,
}) {
if (_flat) {
return Material(
color: color,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: onTap == null && onLongPress == null
? child
: InkWell(onTap: onTap, onLongPress: onLongPress, child: child),
);
}
return GlossyPill(
color: color,
blurSigma: _frost ? AppFrost.sigma : null,
backdropKey: backdropKey,
borderRadius: BorderRadius.circular(27),
onTap: onTap,
onLongPress: onLongPress,
depth: 8,
child: child,
);
} }
Widget _replyPreview(ColorScheme cs) { Widget _replyPreview(ColorScheme cs) {
@@ -465,19 +538,19 @@ class ComposerInputBar extends StatelessWidget {
], ],
), ),
); );
if (chrome != ChatChromeStyle.transparent) return row; if (_flat && _frost) return row;
if (!_frost && chrome != ChatChromeStyle.transparent) return row;
return ClipRect( return ClipRect(
child: BackdropFilter( child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 34, sigmaY: 34), filter: ui.ImageFilter.blur(
sigmaX: AppFrost.sigma,
sigmaY: AppFrost.sigma,
),
backdropGroupKey: backdropKey,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: cs.surface.withValues(alpha: 0.38), color: AppFrost.panelTint(cs),
border: Border( border: Border(top: AppFrost.hairline(cs)),
top: BorderSide(
color: cs.outlineVariant.withValues(alpha: 0.4),
width: 0.5,
),
),
), ),
child: row, child: row,
), ),
+211 -108
View File
@@ -15,7 +15,7 @@ import 'package:komet/backend/modules/upload_notification_service.dart';
import 'package:komet/core/media/gallery_source.dart'; import 'package:komet/core/media/gallery_source.dart';
import 'package:komet/core/utils/format.dart'; import 'package:komet/core/utils/format.dart';
import 'package:komet/frontend/screens/chats/chat_info_screen.dart'; import 'package:komet/frontend/screens/chats/chat_info_screen.dart';
import 'package:komet/frontend/screens/contacts/contact_profile_screen.dart'; import 'package:komet/frontend/screens/contacts/open_contact_profile.dart';
import 'package:komet/frontend/screens/chats/chat_list_screen.dart'; import 'package:komet/frontend/screens/chats/chat_list_screen.dart';
import 'package:komet/frontend/screens/chats/poll_create_screen.dart'; import 'package:komet/frontend/screens/chats/poll_create_screen.dart';
import 'package:komet/frontend/widgets/animated_text_swap.dart'; import 'package:komet/frontend/widgets/animated_text_swap.dart';
@@ -68,6 +68,9 @@ import 'chat/view/shimmer_loading.dart';
import '../../../core/config/app_commands.dart'; import '../../../core/config/app_commands.dart';
import '../../../core/config/app_visual_style.dart'; import '../../../core/config/app_visual_style.dart';
import '../../../core/config/app_chat_chrome.dart'; import '../../../core/config/app_chat_chrome.dart';
import 'package:komet/core/config/app_composer_background.dart';
import 'package:komet/core/config/app_frost.dart';
import 'package:komet/core/config/app_composer_style.dart';
import '../../../core/config/komet_settings.dart'; import '../../../core/config/komet_settings.dart';
import '../../../models/attachment.dart'; import '../../../models/attachment.dart';
import '../../../models/sticker.dart'; import '../../../models/sticker.dart';
@@ -110,15 +113,24 @@ class _UnreadSeparatorItem {
class _FrostedPanel extends StatelessWidget { class _FrostedPanel extends StatelessWidget {
final Color tint; final Color tint;
final Border? border; final Border? border;
final double sigma;
final BackdropKey? backdropKey;
final Widget child; final Widget child;
const _FrostedPanel({required this.tint, this.border, required this.child}); const _FrostedPanel({
required this.tint,
this.border,
this.sigma = AppFrost.panelSigma,
this.backdropKey,
required this.child,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRect( return ClipRect(
child: BackdropFilter( child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 24, sigmaY: 24), filter: ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
backdropGroupKey: backdropKey,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration(color: tint, border: border), decoration: BoxDecoration(color: tint, border: border),
child: child, child: child,
@@ -437,6 +449,9 @@ class _ChatScreenState extends State<ChatScreen>
static const double _glossyHeaderHeight = 76.0; static const double _glossyHeaderHeight = 76.0;
static const double _glossySearchHeight = 58.0; static const double _glossySearchHeight = 58.0;
static const double _pinnedBannerLift = 6.0; static const double _pinnedBannerLift = 6.0;
final BackdropKey _barBackdrop = BackdropKey();
final BackdropKey _pillBackdrop = BackdropKey();
bool get _isLoadingMore => _chatController.isLoadingMore; bool get _isLoadingMore => _chatController.isLoadingMore;
set _isLoadingMore(bool v) => _chatController.isLoadingMore = v; set _isLoadingMore(bool v) => _chatController.isLoadingMore = v;
bool get _hasMoreHistory => _chatController.hasMoreHistory; bool get _hasMoreHistory => _chatController.hasMoreHistory;
@@ -450,6 +465,12 @@ class _ChatScreenState extends State<ChatScreen>
bool _peerIsBot = false; bool _peerIsBot = false;
ChatWallpaper? _wallpaper; ChatWallpaper? _wallpaper;
bool get _composerFrosted =>
AppComposerBackground.current.value == ComposerBackground.frostBlur;
bool get _composerUnderlap =>
AppChatChrome.current.value != ChatChromeStyle.color || _composerFrosted;
ChatChromeStyle get _effectiveChrome { ChatChromeStyle get _effectiveChrome {
final chrome = AppChatChrome.current.value; final chrome = AppChatChrome.current.value;
if (_wallpaper != null && chrome == ChatChromeStyle.none) { if (_wallpaper != null && chrome == ChatChromeStyle.none) {
@@ -510,6 +531,8 @@ class _ChatScreenState extends State<ChatScreen>
_scrollController.addListener(_exitTextSelectionOnScroll); _scrollController.addListener(_exitTextSelectionOnScroll);
AppVisualStyle.current.addListener(_onVisualStyleChanged); AppVisualStyle.current.addListener(_onVisualStyleChanged);
AppChatChrome.current.addListener(_onVisualStyleChanged); AppChatChrome.current.addListener(_onVisualStyleChanged);
AppComposerStyle.current.addListener(_onVisualStyleChanged);
AppComposerBackground.current.addListener(_onVisualStyleChanged);
_shimmerController = AnimationController( _shimmerController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 1500), duration: const Duration(milliseconds: 1500),
@@ -898,7 +921,7 @@ class _ChatScreenState extends State<ChatScreen>
final ua = _unreadAnchorTime; final ua = _unreadAnchorTime;
return ua != null && _messages.indexWhere((m) => m.time > ua) > 0; return ua != null && _messages.indexWhere((m) => m.time > ua) > 0;
}, },
maxPages: 80, maxPages: 15,
); );
if (!mounted) return; if (!mounted) return;
if (_unreadAnchorTime == null) _resolveCountBasedAnchor(); if (_unreadAnchorTime == null) _resolveCountBasedAnchor();
@@ -1161,27 +1184,14 @@ class _ChatScreenState extends State<ChatScreen>
void _jumpToPinnedMessage() { void _jumpToPinnedMessage() {
final id = chat?.pinnedMsgId; final id = chat?.pinnedMsgId;
final time = chat?.pinnedMsgTime;
if (id == null) return; if (id == null) return;
unawaited(_openPinnedMessage(id.toString(), time ?? 0)); final messageId = id.toString();
}
Future<void> _openPinnedMessage(String messageId, int time) async {
if (!_messages.any((m) => m.id == messageId)) {
await _walkHistoryBack(
reached: () => _messages.any((m) => m.id == messageId),
maxPages: 60,
targetTime: time,
);
if (!mounted) return;
await WidgetsBinding.instance.endOfFrame;
if (!mounted) return;
}
if (_messages.any((m) => m.id == messageId)) { if (_messages.any((m) => m.id == messageId)) {
_scrollToLoadedMessage(messageId); _scrollToLoadedMessage(messageId);
} else { return;
showCustomNotification(context, 'Сообщение не загружено');
} }
setState(_beginTargetNavigation);
unawaited(_runGoToMessage(messageId, chat?.pinnedMsgTime ?? 0));
} }
bool _badgeRefreshing = false; bool _badgeRefreshing = false;
@@ -1269,7 +1279,9 @@ class _ChatScreenState extends State<ChatScreen>
void _maybeLoadMoreHistory() { void _maybeLoadMoreHistory() {
if (!_scrollController.hasClients) return; if (!_scrollController.hasClients) return;
if (_historyAutoloadSuppressed) return; if (_historyAutoloadSuppressed) return;
if (_isLoading || _isLoadingMore || !_hasMoreHistory) return; if (_isLoading) return;
_maybeFillGap();
if (_isLoadingMore || !_hasMoreHistory) return;
if (_messages.isEmpty) return; if (_messages.isEmpty) return;
final pos = _scrollController.position; final pos = _scrollController.position;
if (pos.maxScrollExtent <= 0) return; if (pos.maxScrollExtent <= 0) return;
@@ -1278,6 +1290,75 @@ class _ChatScreenState extends State<ChatScreen>
} }
} }
void _maybeFillGap() {
final controller = _chatController;
if (!controller.hasGap || controller.loadingGap) return;
for (final gap in controller.gaps) {
final box = _keyForMessage(gap.edgeId).currentContext?.findRenderObject();
if (box is RenderBox && box.attached) {
unawaited(_fillGapForward(gap));
return;
}
}
}
Future<void> _fillGapForward(HistoryGap gap) async {
final edgeId = gap.edgeId;
final beforeDy = _messageOffsetInList(edgeId);
final added = await _chatController.fillGapForward(gap);
if (!mounted || added == 0) return;
_syncReactionNotifiersFromMessages();
_bumpMessages();
await WidgetsBinding.instance.endOfFrame;
if (!mounted || !_scrollController.hasClients) return;
final afterDy = _messageOffsetInList(edgeId);
if (beforeDy != null && afterDy != null) {
final delta = beforeDy - afterDy;
if (delta.abs() > 0.5) {
final pos = _scrollController.position;
_scrollController.jumpTo(
(pos.pixels + delta).clamp(pos.minScrollExtent, pos.maxScrollExtent),
);
}
}
_loadForwardedSenderNames();
_loadGroupSenderNames();
}
double? _messageOffsetInList(String messageId) {
final listBox = _listKey.currentContext?.findRenderObject();
final box = _keyForMessage(messageId).currentContext?.findRenderObject();
if (listBox is! RenderBox || box is! RenderBox || !box.attached) return null;
return box.localToGlobal(Offset.zero, ancestor: listBox).dy;
}
Future<void> _loadMessageWindow(String messageId, int targetTime) async {
if (targetTime <= 0) {
await _walkHistoryBack(
reached: () => _messages.any((m) => m.id == messageId),
maxPages: 10,
);
return;
}
_historyAutoloadSuppressCount++;
try {
await _chatController.loadMessageWindow(
targetId: messageId,
targetTime: targetTime,
);
} finally {
_historyAutoloadSuppressCount--;
}
if (!mounted) return;
_syncReactionNotifiersFromMessages();
_bumpMessages();
_loadForwardedSenderNames();
_loadGroupSenderNames();
}
Future<void> _walkHistoryBack({ Future<void> _walkHistoryBack({
required bool Function() reached, required bool Function() reached,
required int maxPages, required int maxPages,
@@ -1294,7 +1375,11 @@ class _ChatScreenState extends State<ChatScreen>
(_messages.isEmpty || _messages.first.time > targetTime)) { (_messages.isEmpty || _messages.first.time > targetTime)) {
page++; page++;
final before = _messages.isEmpty ? 0 : _messages.first.time; final before = _messages.isEmpty ? 0 : _messages.first.time;
await _loadMoreHistory(resolveSenderNames: false); await _loadMoreHistory(
resolveSenderNames: false,
pageSize: ChatController.historyWalkPageSize,
persist: false,
);
if (!mounted) return; if (!mounted) return;
final after = _messages.isEmpty ? 0 : _messages.first.time; final after = _messages.isEmpty ? 0 : _messages.first.time;
if (after == before) break; if (after == before) break;
@@ -1303,12 +1388,19 @@ class _ChatScreenState extends State<ChatScreen>
_historyAutoloadSuppressCount--; _historyAutoloadSuppressCount--;
} }
if (!mounted) return; if (!mounted) return;
_chatController.persistSessionCache();
_loadForwardedSenderNames(); _loadForwardedSenderNames();
_loadGroupSenderNames(); _loadGroupSenderNames();
} }
Future<void> _loadMoreHistory({bool resolveSenderNames = true}) async { Future<void> _loadMoreHistory({
bool resolveSenderNames = true,
int? pageSize,
bool persist = true,
}) async {
await _chatController.loadMoreHistory( await _chatController.loadMoreHistory(
pageSize: pageSize,
persist: persist,
onLoadingStarted: _bumpMessages, onLoadingStarted: _bumpMessages,
onLoaded: (added) { onLoaded: (added) {
if (added > 0) _syncReactionNotifiersFromMessages(); if (added > 0) _syncReactionNotifiersFromMessages();
@@ -1411,6 +1503,8 @@ class _ChatScreenState extends State<ChatScreen>
_readMarkTimer?.cancel(); _readMarkTimer?.cancel();
AppVisualStyle.current.removeListener(_onVisualStyleChanged); AppVisualStyle.current.removeListener(_onVisualStyleChanged);
AppChatChrome.current.removeListener(_onVisualStyleChanged); AppChatChrome.current.removeListener(_onVisualStyleChanged);
AppComposerStyle.current.removeListener(_onVisualStyleChanged);
AppComposerBackground.current.removeListener(_onVisualStyleChanged);
_composerHeight.dispose(); _composerHeight.dispose();
_pinnedBannerHeight.dispose(); _pinnedBannerHeight.dispose();
_floatingDateTimer?.cancel(); _floatingDateTimer?.cancel();
@@ -1970,6 +2064,9 @@ class _ChatScreenState extends State<ChatScreen>
ComposerInputBar( ComposerInputBar(
chatType: widget.chatType, chatType: widget.chatType,
chrome: _effectiveChrome, chrome: _effectiveChrome,
style: AppComposerStyle.current.value,
background: AppComposerBackground.current.value,
backdropKey: _pillBackdrop,
attachAnim: _attachAnim, attachAnim: _attachAnim,
replyTo: _replyTo, replyTo: _replyTo,
myId: _myId, myId: _myId,
@@ -2028,15 +2125,23 @@ class _ChatScreenState extends State<ChatScreen>
], ],
); );
Widget wrapChrome(Widget child) { Widget wrapChrome(Widget child) {
if (_composerFrosted) {
if (AppComposerStyle.current.value != ComposerStyle.materialYou) {
return child;
}
return _FrostedPanel(
sigma: AppFrost.sigma,
tint: AppFrost.panelTint(cs),
border: Border(top: AppFrost.hairline(cs)),
backdropKey: _barBackdrop,
child: child,
);
}
if (_effectiveChrome != ChatChromeStyle.blur) return child; if (_effectiveChrome != ChatChromeStyle.blur) return child;
return _FrostedPanel( return _FrostedPanel(
tint: cs.surfaceContainerHigh.withValues(alpha: 0.55), tint: AppFrost.blurPanelTint(cs),
border: Border( border: Border(top: AppFrost.hairline(cs)),
top: BorderSide( backdropKey: _barBackdrop,
color: cs.outlineVariant.withValues(alpha: 0.4),
width: 0.5,
),
),
child: child, child: child,
); );
} }
@@ -2398,13 +2503,9 @@ class _ChatScreenState extends State<ChatScreen>
: Colors.transparent, : Colors.transparent,
flexibleSpace: chrome == ChatChromeStyle.blur flexibleSpace: chrome == ChatChromeStyle.blur
? _FrostedPanel( ? _FrostedPanel(
tint: cs.surfaceContainerHigh.withValues(alpha: 0.55), tint: AppFrost.blurPanelTint(cs),
border: Border( border: Border(bottom: AppFrost.hairline(cs)),
bottom: BorderSide( backdropKey: _barBackdrop,
color: cs.outlineVariant.withValues(alpha: 0.4),
width: 0.5,
),
),
child: const SizedBox.expand(), child: const SizedBox.expand(),
) )
: (chrome == ChatChromeStyle.none && !glossy) : (chrome == ChatChromeStyle.none && !glossy)
@@ -2426,22 +2527,12 @@ class _ChatScreenState extends State<ChatScreen>
), ),
) )
: (chrome == ChatChromeStyle.transparent && !glossy) : (chrome == ChatChromeStyle.transparent && !glossy)
? IgnorePointer( ? _FrostedPanel(
child: DecoratedBox( sigma: AppFrost.sigma,
decoration: BoxDecoration( tint: AppFrost.panelTint(cs),
gradient: LinearGradient( border: Border(bottom: AppFrost.hairline(cs)),
begin: Alignment.topCenter, backdropKey: _barBackdrop,
end: Alignment.bottomCenter, child: const SizedBox.expand(),
colors: [
cs.surface.withValues(alpha: 0.72),
cs.surface.withValues(alpha: 0.45),
cs.surface.withValues(alpha: 0.0),
],
stops: const [0.0, 0.62, 1.0],
),
),
child: const SizedBox.expand(),
),
) )
: null, : null,
foregroundColor: cs.onSurface, foregroundColor: cs.onSurface,
@@ -2477,6 +2568,10 @@ class _ChatScreenState extends State<ChatScreen>
offset: Offset(0, -height * 0.4 * t), offset: Offset(0, -height * 0.4 * t),
child: ChatHeaderRow( child: ChatHeaderRow(
glossy: glossy, glossy: glossy,
frosted:
glossy &&
chrome == ChatChromeStyle.transparent,
backdropKey: _pillBackdrop,
cs: cs, cs: cs,
embedded: widget.embedded, embedded: widget.embedded,
chatId: widget.chatId, chatId: widget.chatId,
@@ -3482,14 +3577,12 @@ class _ChatScreenState extends State<ChatScreen>
void _openSenderProfile(int senderId) { void _openSenderProfile(int senderId) {
if (senderId == 0 || senderId == _myId) return; if (senderId == 0 || senderId == _myId) return;
Navigator.push( unawaited(
context, openContactDialogProfile(
MaterialPageRoute( context,
builder: (_) => ContactProfileScreen( contactId: senderId,
contactId: senderId, name: ContactCache.get(senderId) ?? 'User #$senderId',
initialName: ContactCache.get(senderId), avatarUrl: ContactCache.getAvatar(senderId),
initialAvatarUrl: ContactCache.getAvatar(senderId),
),
), ),
); );
} }
@@ -3570,11 +3663,7 @@ class _ChatScreenState extends State<ChatScreen>
if (!mounted) return; if (!mounted) return;
if (!_messages.any((m) => m.id == id)) { if (!_messages.any((m) => m.id == id)) {
await _walkHistoryBack( await _loadMessageWindow(id, targetTime);
reached: () => _messages.any((m) => m.id == id),
maxPages: 80,
targetTime: targetTime,
);
if (!mounted) return; if (!mounted) return;
await WidgetsBinding.instance.endOfFrame; await WidgetsBinding.instance.endOfFrame;
if (!mounted) return; if (!mounted) return;
@@ -3684,29 +3773,14 @@ class _ChatScreenState extends State<ChatScreen>
Future<void> _openSearchResult(MessageSearchResult result) async { Future<void> _openSearchResult(MessageSearchResult result) async {
_closeSearch(); _closeSearch();
await WidgetsBinding.instance.endOfFrame; if (_messages.any((m) => m.id == result.id)) {
if (!mounted) return;
if (!_messages.any((m) => m.id == result.id)) {
var guard = 0;
while (mounted &&
guard < 60 &&
_hasMoreHistory &&
!_messages.any((m) => m.id == result.id) &&
(_messages.isEmpty || _messages.first.time > result.time)) {
guard++;
final before = _messages.isEmpty ? 0 : _messages.first.time;
await _loadMoreHistory();
if (!mounted) return;
final after = _messages.isEmpty ? 0 : _messages.first.time;
if (after == before) break;
}
if (!mounted) return;
await WidgetsBinding.instance.endOfFrame; await WidgetsBinding.instance.endOfFrame;
if (!mounted) return; if (!mounted) return;
_scrollToLoadedMessage(result.id);
return;
} }
setState(_beginTargetNavigation);
_scrollToLoadedMessage(result.id); await _runGoToMessage(result.id, result.time);
} }
void _scrollToLoadedMessage( void _scrollToLoadedMessage(
@@ -4129,6 +4203,8 @@ class _ChatScreenState extends State<ChatScreen>
text: pinned.pinnedMsgText, text: pinned.pinnedMsgText,
isPreview: pinned.pinnedMsgIsPreview, isPreview: pinned.pinnedMsgIsPreview,
floating: floating, floating: floating,
frosted: _effectiveChrome == ChatChromeStyle.transparent,
backdropKey: _pillBackdrop,
onTap: _jumpToPinnedMessage, onTap: _jumpToPinnedMessage,
onUnpin: pinned.canPinMessages(_myId) onUnpin: pinned.canPinMessages(_myId)
? () => unawaited(_unpinCurrentMessage()) ? () => unawaited(_unpinCurrentMessage())
@@ -4139,6 +4215,11 @@ class _ChatScreenState extends State<ChatScreen>
Widget _buildColorBody() { Widget _buildColorBody() {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final banner = _buildPinnedBanner(floating: false); final banner = _buildPinnedBanner(floating: false);
final frosted = _composerFrosted;
final composer = _MeasureSize(
onHeight: (value) => _composerHeight.value = value,
child: _buildComposerArea(context),
);
return Column( return Column(
children: [ children: [
?banner, ?banner,
@@ -4151,12 +4232,17 @@ class _ChatScreenState extends State<ChatScreen>
child: ChatWallpaperView(wallpaper: _wallpaper!), child: ChatWallpaperView(wallpaper: _wallpaper!),
), ),
Positioned.fill(child: _buildMessagesArea()), Positioned.fill(child: _buildMessagesArea()),
Positioned( ValueListenableBuilder<double>(
left: 0, valueListenable: _composerHeight,
right: 0, builder: (context, height, _) => Positioned(
bottom: 0, left: 0,
child: CommandPanelView(commandPanel: _commandPanel), right: 0,
bottom: frosted ? height : 0,
child: CommandPanelView(commandPanel: _commandPanel),
),
), ),
if (frosted)
Positioned(left: 0, right: 0, bottom: 0, child: composer),
SearchOverlay( SearchOverlay(
cs: cs, cs: cs,
searchAnim: _searchAnim, searchAnim: _searchAnim,
@@ -4168,7 +4254,7 @@ class _ChatScreenState extends State<ChatScreen>
], ],
), ),
), ),
_buildComposerArea(context), if (!frosted) composer,
], ],
); );
} }
@@ -4391,13 +4477,8 @@ class _ChatScreenState extends State<ChatScreen>
if (index == 0) { if (index == 0) {
return ValueListenableBuilder<double>( return ValueListenableBuilder<double>(
valueListenable: _composerHeight, valueListenable: _composerHeight,
builder: (context, height, _) => SizedBox( builder: (context, height, _) =>
height: SizedBox(height: _composerUnderlap ? height : 0),
AppChatChrome.current.value ==
ChatChromeStyle.color
? 0
: height,
),
); );
} }
if (index > items.length) { if (index > items.length) {
@@ -5609,6 +5690,8 @@ class _PinnedMessageBanner extends StatelessWidget {
final VoidCallback onTap; final VoidCallback onTap;
final VoidCallback? onUnpin; final VoidCallback? onUnpin;
final bool floating; final bool floating;
final bool frosted;
final BackdropKey? backdropKey;
const _PinnedMessageBanner({ const _PinnedMessageBanner({
required this.text, required this.text,
@@ -5616,13 +5699,17 @@ class _PinnedMessageBanner extends StatelessWidget {
required this.onTap, required this.onTap,
this.onUnpin, this.onUnpin,
this.floating = false, this.floating = false,
this.frosted = false,
this.backdropKey,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final content = Material( final content = Material(
color: floating color: frosted
? AppFrost.panelTint(cs)
: floating
? cs.surfaceContainerHigh.withValues(alpha: 0.92) ? cs.surfaceContainerHigh.withValues(alpha: 0.92)
: cs.surfaceContainerHigh, : cs.surfaceContainerHigh,
borderRadius: floating ? BorderRadius.circular(16) : null, borderRadius: floating ? BorderRadius.circular(16) : null,
@@ -5679,16 +5766,32 @@ class _PinnedMessageBanner extends StatelessWidget {
), ),
); );
if (!floating) { final bottomBorder = Border(bottom: AppFrost.hairline(cs));
return DecoratedBox(
decoration: BoxDecoration( if (frosted) {
border: Border( return ClipRRect(
bottom: BorderSide( borderRadius: floating
color: cs.outlineVariant.withValues(alpha: 0.4), ? BorderRadius.circular(16)
width: 0.5, : BorderRadius.zero,
child: BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: AppFrost.sigma,
sigmaY: AppFrost.sigma,
),
backdropGroupKey: backdropKey,
child: DecoratedBox(
decoration: BoxDecoration(
border: floating ? null : bottomBorder,
), ),
child: content,
), ),
), ),
);
}
if (!floating) {
return DecoratedBox(
decoration: BoxDecoration(border: bottomBorder),
child: content, child: content,
); );
} }
+13 -15
View File
@@ -11,7 +11,7 @@ import '../../../core/utils/debouncer.dart';
import '../../../core/utils/names.dart'; import '../../../core/utils/names.dart';
import '../../widgets/komet_avatar.dart'; import '../../widgets/komet_avatar.dart';
import '../../widgets/swipe_route.dart'; import '../../widgets/swipe_route.dart';
import '../contacts/contact_profile_screen.dart'; import '../contacts/open_contact_profile.dart';
import 'chat_screen.dart'; import 'chat_screen.dart';
class SearchScreen extends StatefulWidget { class SearchScreen extends StatefulWidget {
@@ -147,13 +147,12 @@ class _SearchScreenState extends State<SearchScreen> {
} }
void _openContact(Map<String, dynamic> row) { void _openContact(Map<String, dynamic> row) {
Navigator.of(context).push( unawaited(
MaterialPageRoute( openContactDialogProfile(
builder: (_) => ContactProfileScreen( context,
contactId: row['id'] as int, contactId: row['id'] as int,
initialName: _contactName(row), name: _contactName(row),
initialAvatarUrl: row['base_url'] as String?, avatarUrl: row['base_url'] as String?,
),
), ),
); );
} }
@@ -166,13 +165,12 @@ class _SearchScreenState extends State<SearchScreen> {
} }
void _openPhoneResult(PhoneLookupResult result) { void _openPhoneResult(PhoneLookupResult result) {
Navigator.of(context).push( unawaited(
MaterialPageRoute( openContactDialogProfile(
builder: (_) => ContactProfileScreen( context,
contactId: result.id, contactId: result.id,
initialName: result.name, name: result.name ?? 'User #${result.id}',
initialAvatarUrl: result.avatarUrl, avatarUrl: result.avatarUrl,
),
), ),
); );
} }
@@ -1,420 +0,0 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/cache/info_cache.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/storage/token_storage.dart';
import '../../../core/utils/format.dart';
import '../../../l10n/app_localizations.dart';
import '../../../models/contact_info.dart';
import '../../widgets/avatar_history_screen.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/swipe_route.dart';
import '../chats/chat_screen.dart';
class ContactProfileScreen extends StatefulWidget {
final int contactId;
final String? initialName;
final String? initialAvatarUrl;
const ContactProfileScreen({
super.key,
required this.contactId,
this.initialName,
this.initialAvatarUrl,
});
@override
State<ContactProfileScreen> createState() => _ContactProfileScreenState();
}
class _ContactProfileScreenState extends State<ContactProfileScreen> {
bool _loading = true;
ContactInfo? _contact;
int? _seenTime;
int _presenceStatus = 0;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final contactFuture = ContactInfoFetch.get(widget.contactId);
final presenceFuture = PresenceFetch.get(widget.contactId);
final contact = await contactFuture;
final presence = await presenceFuture;
if (!mounted) return;
if (contact != null) {
_contact = contact;
}
if (presence != null) {
_seenTime = presence['seen'] as int?;
_presenceStatus = (presence['status'] as int?) ?? 0;
}
} catch (e) {
if (mounted) {
showCustomNotification(
context,
AppLocalizations.of(context)!.contactProfileLoadError(e.toString()),
);
}
} finally {
if (mounted) setState(() => _loading = false);
}
}
String _displayName() {
return _contact?.displayName ??
widget.initialName ??
'User #${widget.contactId}';
}
String? _avatarUrl() {
return _contact?.avatarUrl ?? widget.initialAvatarUrl;
}
Set<String> _options() {
return _contact?.options.toSet() ?? const {};
}
bool get _isBot => _options().contains('BOT');
bool get _isVerified => _options().contains('OFFICIAL');
String _subtitle() {
final l10n = AppLocalizations.of(context)!;
if (_isBot) return l10n.contactProfileBot;
if (_presenceStatus == 1) return l10n.contactProfileOnline;
if (_presenceStatus == 2 || _presenceStatus == 3) return l10n.contactProfileRecentlyActive;
if (_seenTime != null && _seenTime! > 0) return formatLastSeen(_seenTime!);
return '';
}
Future<void> _openChat() async {
final accountId = await TokenStorage.getActiveAccountId();
if (accountId == null) return;
final existing = await AppDatabase.findDialogChatByParticipant(
accountId,
widget.contactId,
);
final chatId = existing ?? (accountId ^ widget.contactId);
if (!mounted) return;
pushSwipeable(
context,
(_) => ChatScreen(
chatId: chatId,
name: _displayName(),
imageUrl: _avatarUrl() ?? '',
chatType: 'DIALOG',
),
);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
floatingActionButton: const ConnectionSpinner(),
body: SafeArea(
child: _loading
? const Center(child: CircularProgressIndicator())
: _buildBody(cs),
),
);
}
Widget _buildBody(ColorScheme cs) {
return CustomScrollView(
slivers: [
SliverAppBar(
backgroundColor: Colors.transparent,
elevation: 0,
floating: true,
leading: IconButton(
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
onPressed: () => Navigator.pop(context),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
GestureDetector(
onTap: () => AvatarHistoryScreen.open(
context,
contactId: widget.contactId,
name: _displayName(),
currentAvatarUrl: _avatarUrl(),
),
child: KometAvatar(
name: _displayName(),
imageUrl: _avatarUrl(),
size: 96,
fontSize: 36,
),
),
const SizedBox(height: 14),
_buildNameRow(cs),
const SizedBox(height: 4),
Text(
_subtitle(),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
),
const SizedBox(height: 20),
_buildActions(cs),
const SizedBox(height: 16),
_buildInfoCard(cs),
const SizedBox(height: 40),
],
),
),
),
],
);
}
Widget _buildNameRow(ColorScheme cs) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
_displayName(),
style: TextStyle(
color: cs.onSurface,
fontSize: 22,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (_isVerified) ...[
const SizedBox(width: 6),
Icon(Symbols.verified, color: cs.primary, size: 20, fill: 1),
],
],
);
}
Widget _buildActions(ColorScheme cs) {
final l10n = AppLocalizations.of(context)!;
final actions = <({IconData icon, String label, VoidCallback? onTap})>[
(
icon: Symbols.chat_bubble,
label: l10n.contactProfileActionChat,
onTap: _openChat,
),
(
icon: Symbols.notifications,
label: l10n.contactProfileActionSound,
onTap: null,
),
if (!_isBot)
(icon: Symbols.call, label: l10n.contactProfileActionCall, onTap: null),
];
return Row(
children: [
for (var i = 0; i < actions.length; i++) ...[
Expanded(
child: GestureDetector(
onTap: actions[i].onTap,
child: GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric(vertical: 12),
depth: 6,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(actions[i].icon, color: cs.primary, size: 22),
const SizedBox(height: 4),
Text(
actions[i].label,
style: TextStyle(color: cs.onSurface, fontSize: 12),
),
],
),
),
),
),
if (i < actions.length - 1) const SizedBox(width: 8),
],
],
);
}
Widget _buildInfoCard(ColorScheme cs) {
final c = _contact;
if (c == null) return const SizedBox.shrink();
final l10n = AppLocalizations.of(context)!;
final rows = <Widget>[];
final phoneStr = formatPhone(c.raw['phone']);
if (phoneStr != null) {
rows.add(
_infoRow(cs, Symbols.phone, l10n.contactProfileInfoPhone, phoneStr),
);
}
final country = c.raw['country'] as String?;
if (country != null && country.isNotEmpty) {
rows.add(
_infoRow(cs, Symbols.public, l10n.contactProfileInfoCountry, country),
);
}
final genderStr = formatGender(c.raw['gender']);
if (genderStr != null) {
rows.add(
_infoRow(cs, Symbols.wc, l10n.contactProfileInfoGender, genderStr),
);
}
final regTime = c.raw['registrationTime'] as int?;
if (regTime != null && regTime > 0) {
rows.add(
_infoRow(
cs,
Symbols.event,
l10n.contactProfileInfoRegistration,
formatDateTimeWords(DateTime.fromMillisecondsSinceEpoch(regTime)),
),
);
}
final updateTime = c.raw['updateTime'] as int?;
if (updateTime != null && updateTime > 0) {
rows.add(
_infoRow(
cs,
Symbols.update,
l10n.contactProfileInfoUpdated,
formatDateTimeWords(DateTime.fromMillisecondsSinceEpoch(updateTime)),
),
);
}
final accountStatus = c.raw['accountStatus'];
if (accountStatus is int && accountStatus != 0) {
rows.add(
_infoRow(
cs,
Symbols.account_circle,
l10n.contactProfileInfoAccountStatus,
accountStatus.toString(),
),
);
}
final desc = (c.raw['description'] as String?)?.trim();
if (desc != null && desc.isNotEmpty) {
rows.add(
_infoRow(
cs,
Symbols.info,
l10n.contactProfileInfoDescription,
desc,
multiline: true,
),
);
}
final link = c.raw['link'] as String?;
if (link != null && link.isNotEmpty) {
rows.add(_infoRow(cs, Symbols.link, l10n.contactProfileInfoLink, link));
}
final webApp = c.raw['webApp'] as String?;
if (webApp != null && webApp.isNotEmpty) {
rows.add(_infoRow(cs, Symbols.web, 'Web app', webApp));
}
final opts = _options();
if (opts.isNotEmpty) {
rows.add(
_infoRow(
cs,
Symbols.label,
l10n.contactProfileInfoFlags,
opts.join(', '),
multiline: true,
),
);
}
rows.add(_infoRow(cs, Symbols.tag, 'ID', widget.contactId.toString()));
if (rows.isEmpty) return const SizedBox.shrink();
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
depth: 6,
child: SizedBox(
width: double.infinity,
child: Column(
children: [
for (var i = 0; i < rows.length; i++) ...[
if (i > 0)
Divider(
height: 1,
color: cs.outlineVariant.withValues(alpha: 0.3),
),
rows[i],
],
],
),
),
);
}
Widget _infoRow(
ColorScheme cs,
IconData icon,
String label,
String value, {
bool multiline = false,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: cs.onSurfaceVariant, size: 20),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(color: cs.onSurface, fontSize: 14),
maxLines: multiline ? null : 1,
overflow: multiline ? null : TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}
@@ -13,32 +13,7 @@ import '../../widgets/connection_status.dart';
import '../../widgets/sheet_helpers.dart'; import '../../widgets/sheet_helpers.dart';
import '../chats/chat_info_screen.dart'; import '../chats/chat_info_screen.dart';
import 'nfc_exchange_sheet.dart'; import 'nfc_exchange_sheet.dart';
import 'open_contact_profile.dart';
Future<void> openContactDialogProfile(
BuildContext context, {
required int contactId,
required String name,
String? avatarUrl,
}) async {
final accountId = await TokenStorage.getActiveAccountId();
final existing = accountId == null
? null
: await AppDatabase.findDialogChatByParticipant(accountId, contactId);
final chatId = existing ?? ((accountId ?? 0) ^ contactId);
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChatInfoScreen(
chatId: chatId,
name: name,
imageUrl: avatarUrl ?? '',
chatType: 'DIALOG',
dialogPeerId: contactId,
),
),
);
}
class ContactsTab extends StatefulWidget { class ContactsTab extends StatefulWidget {
const ContactsTab({super.key}); const ContactsTab({super.key});
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/storage/token_storage.dart';
import '../chats/chat_info_screen.dart';
Future<void> openContactDialogProfile(
BuildContext context, {
required int contactId,
required String name,
String? avatarUrl,
}) async {
final accountId = await TokenStorage.getActiveAccountId();
final existing = accountId == null
? null
: await AppDatabase.findDialogChatByParticipant(accountId, contactId);
final chatId = existing ?? ((accountId ?? 0) ^ contactId);
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChatInfoScreen(
chatId: chatId,
name: name,
imageUrl: avatarUrl ?? '',
chatType: 'DIALOG',
dialogPeerId: contactId,
),
),
);
}
@@ -10,6 +10,8 @@ import '../../../core/config/app_bubble_shape.dart';
import '../../../core/config/app_pill_gradient.dart'; import '../../../core/config/app_pill_gradient.dart';
import '../../../core/config/app_visual_style.dart'; import '../../../core/config/app_visual_style.dart';
import '../../../core/config/app_chat_chrome.dart'; import '../../../core/config/app_chat_chrome.dart';
import '../../../core/config/app_composer_background.dart';
import '../../../core/config/app_composer_style.dart';
import '../../../core/utils/bubble_radius.dart'; import '../../../core/utils/bubble_radius.dart';
import '../../../core/utils/debouncer.dart'; import '../../../core/utils/debouncer.dart';
import '../../../core/utils/haptics.dart'; import '../../../core/utils/haptics.dart';
@@ -119,6 +121,8 @@ class _AppearanceScreenState extends State<AppearanceScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
const _ChatChromeCard(), const _ChatChromeCard(),
const SizedBox(height: 12), const SizedBox(height: 12),
const _ComposerBarCard(),
const SizedBox(height: 12),
const _GradientToggleCard(), const _GradientToggleCard(),
], ],
), ),
@@ -253,6 +257,90 @@ class _ChatChromeCard extends StatelessWidget {
} }
} }
class _ComposerBarCard extends StatelessWidget {
const _ComposerBarCard();
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(28),
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
depth: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.appearanceComposerTitle,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
l10n.appearanceComposerSubtitle,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 16),
ValueListenableBuilder<ComposerStyle>(
valueListenable: AppComposerStyle.current,
builder: (context, current, _) {
return SegmentedButton<ComposerStyle>(
segments: [
ButtonSegment(
value: ComposerStyle.glossy,
label: Text(l10n.appearanceVisualStyleGlossy),
),
ButtonSegment(
value: ComposerStyle.materialYou,
label: Text(l10n.appearanceVisualStyleMaterialYou),
),
],
selected: {current},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppComposerStyle.save(set.first);
}
},
);
},
),
const SizedBox(height: 10),
ValueListenableBuilder<ComposerBackground>(
valueListenable: AppComposerBackground.current,
builder: (context, current, _) {
return SegmentedButton<ComposerBackground>(
segments: [
ButtonSegment(
value: ComposerBackground.standard,
label: Text(l10n.appearanceComposerBackgroundStandard),
),
ButtonSegment(
value: ComposerBackground.frostBlur,
label: Text(l10n.appearanceComposerBackgroundFrost),
),
],
selected: {current},
onSelectionChanged: (set) {
if (set.isNotEmpty) {
Haptics.selection();
AppComposerBackground.save(set.first);
}
},
);
},
),
],
),
);
}
}
class _GradientToggleCard extends StatelessWidget { class _GradientToggleCard extends StatelessWidget {
const _GradientToggleCard(); const _GradientToggleCard();
@@ -40,7 +40,7 @@ class _CustomizationSectionState extends State<CustomizationSection> {
builder: (context) => const ThemeSettingsScreen(), builder: (context) => const ThemeSettingsScreen(),
), ),
_CustomizationCategory( _CustomizationCategory(
icon: Symbols.palette, icon: Symbols.styler,
title: 'Внешний вид', title: 'Внешний вид',
builder: (context) => const AppearanceScreen(), builder: (context) => const AppearanceScreen(),
), ),
+29 -1
View File
@@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/config/app_pill_gradient.dart'; import '../../core/config/app_pill_gradient.dart';
@@ -90,6 +92,8 @@ class GlossyPill extends StatelessWidget {
final double depth; final double depth;
final bool elevated; final bool elevated;
final BorderSide? borderSide; final BorderSide? borderSide;
final double? blurSigma;
final BackdropKey? backdropKey;
const GlossyPill({ const GlossyPill({
super.key, super.key,
@@ -102,9 +106,14 @@ class GlossyPill extends StatelessWidget {
this.depth = 10, this.depth = 10,
this.elevated = false, this.elevated = false,
this.borderSide, this.borderSide,
this.blurSigma,
this.backdropKey,
}) : borderRadius = }) : borderRadius =
borderRadius ?? const BorderRadius.all(Radius.circular(100)); borderRadius ?? const BorderRadius.all(Radius.circular(100));
double? _sigmaFor(Color base) =>
blurSigma != null && base.a < 1 ? blurSigma : null;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder<VisualStyle>( return ValueListenableBuilder<VisualStyle>(
@@ -123,7 +132,7 @@ class GlossyPill extends StatelessWidget {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final base = color ?? cs.surfaceContainerHigh; final base = color ?? cs.surfaceContainerHigh;
final content = Padding(padding: padding, child: child); final content = Padding(padding: padding, child: child);
return Material( final material = Material(
color: base, color: base,
elevation: elevated ? 3 : 0, elevation: elevated ? 3 : 0,
shadowColor: Colors.black.withValues(alpha: 0.4), shadowColor: Colors.black.withValues(alpha: 0.4),
@@ -137,12 +146,23 @@ class GlossyPill extends StatelessWidget {
? content ? content
: InkWell(onTap: onTap, onLongPress: onLongPress, child: content), : InkWell(onTap: onTap, onLongPress: onLongPress, child: content),
); );
final sigma = _sigmaFor(base);
if (sigma == null) return material;
return ClipRRect(
borderRadius: borderRadius,
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
backdropGroupKey: backdropKey,
child: material,
),
);
} }
Widget _glossy(BuildContext context, bool gradient) { Widget _glossy(BuildContext context, bool gradient) {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final base = color ?? cs.surfaceContainerHigh; final base = color ?? cs.surfaceContainerHigh;
final content = Padding(padding: padding, child: child); final content = Padding(padding: padding, child: child);
final sigma = _sigmaFor(base);
return RepaintBoundary( return RepaintBoundary(
child: DecoratedBox( child: DecoratedBox(
@@ -158,6 +178,14 @@ class GlossyPill extends StatelessWidget {
child: Stack( child: Stack(
fit: StackFit.passthrough, fit: StackFit.passthrough,
children: [ children: [
if (sigma != null)
Positioned.fill(
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
backdropGroupKey: backdropKey,
child: const SizedBox.expand(),
),
),
if (gradient) ...[ if (gradient) ...[
Positioned.fill( Positioned.fill(
child: IgnorePointer( child: IgnorePointer(
+9 -8
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../backend/modules/chats.dart'; import '../../backend/modules/chats.dart';
@@ -6,7 +8,7 @@ import '../../core/links/max_link.dart';
import '../../core/storage/app_database.dart'; import '../../core/storage/app_database.dart';
import '../../main.dart'; import '../../main.dart';
import '../screens/chats/chat_screen.dart'; import '../screens/chats/chat_screen.dart';
import '../screens/contacts/contact_profile_screen.dart'; import '../screens/contacts/open_contact_profile.dart';
import 'call_link_handler.dart'; import 'call_link_handler.dart';
import 'confirm_dialog.dart'; import 'confirm_dialog.dart';
import 'custom_notification.dart'; import 'custom_notification.dart';
@@ -75,13 +77,12 @@ void _openContact(BuildContext context, Map<dynamic, dynamic> contact) {
showCustomNotification(context, 'Не удалось открыть профиль'); showCustomNotification(context, 'Не удалось открыть профиль');
return; return;
} }
Navigator.of(context).push( unawaited(
MaterialPageRoute( openContactDialogProfile(
builder: (_) => ContactProfileScreen( context,
contactId: id, contactId: id,
initialName: _contactName(contact), name: _contactName(contact),
initialAvatarUrl: contact['baseUrl'] as String?, avatarUrl: contact['baseUrl'] as String?,
),
), ),
); );
} }
@@ -494,19 +494,19 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
final hasText = final hasText =
widget.messageText != null && widget.messageText!.isNotEmpty; widget.messageText != null && widget.messageText!.isNotEmpty;
return <_Action>[ return <_Action>[
if (widget.onReply != null)
_Action(Symbols.reply, l10n.msgActionsReply, _reply),
if (widget.onForward != null)
_Action(Symbols.forward, l10n.msgActionsForward, _forward),
if (hasText) _Action(Symbols.content_copy, l10n.msgActionsCopy, _copy), if (hasText) _Action(Symbols.content_copy, l10n.msgActionsCopy, _copy),
if (widget.isMe && widget.onEdit != null) if (widget.isMe && widget.onEdit != null)
_Action(Symbols.edit, l10n.msgActionsEdit, _edit), _Action(Symbols.edit, l10n.msgActionsEdit, _edit),
if (widget.onReply != null)
_Action(Symbols.reply, l10n.msgActionsReply, _reply),
if (widget.onPin != null) if (widget.onPin != null)
_Action( _Action(
widget.isPinned ? Symbols.keep_off : Symbols.push_pin, widget.isPinned ? Symbols.keep_off : Symbols.push_pin,
widget.isPinned ? l10n.msgActionsUnpin : l10n.msgActionsPin, widget.isPinned ? l10n.msgActionsUnpin : l10n.msgActionsPin,
_pin, _pin,
), ),
if (widget.onForward != null)
_Action(Symbols.forward, l10n.msgActionsForward, _forward),
if (widget.onMarkUnread != null) if (widget.onMarkUnread != null)
_Action( _Action(
Symbols.mark_chat_unread, Symbols.mark_chat_unread,
+5 -1
View File
@@ -295,7 +295,11 @@
"appearanceChatChromeColor": "Color", "appearanceChatChromeColor": "Color",
"appearanceChatChromeBlur": "Blur", "appearanceChatChromeBlur": "Blur",
"appearanceChatChromeNone": "None", "appearanceChatChromeNone": "None",
"appearanceChatChromeTransparent": "Clear", "appearanceChatChromeTransparent": "Frost blur",
"appearanceComposerTitle": "Input bar",
"appearanceComposerSubtitle": "Style and background of the message input bar",
"appearanceComposerBackgroundStandard": "Default",
"appearanceComposerBackgroundFrost": "Frost blur",
"appearanceGradientTitle": "Gradient", "appearanceGradientTitle": "Gradient",
"appearanceGradientSubtitle": "Depth and highlights in Glossy capsules", "appearanceGradientSubtitle": "Depth and highlights in Glossy capsules",
"appearanceAccentColorTitle": "Accent color", "appearanceAccentColorTitle": "Accent color",
+25 -1
View File
@@ -1559,9 +1559,33 @@ abstract class AppLocalizations {
/// No description provided for @appearanceChatChromeTransparent. /// No description provided for @appearanceChatChromeTransparent.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Clear'** /// **'Frost blur'**
String get appearanceChatChromeTransparent; String get appearanceChatChromeTransparent;
/// No description provided for @appearanceComposerTitle.
///
/// In en, this message translates to:
/// **'Input bar'**
String get appearanceComposerTitle;
/// No description provided for @appearanceComposerSubtitle.
///
/// In en, this message translates to:
/// **'Style and background of the message input bar'**
String get appearanceComposerSubtitle;
/// No description provided for @appearanceComposerBackgroundStandard.
///
/// In en, this message translates to:
/// **'Default'**
String get appearanceComposerBackgroundStandard;
/// No description provided for @appearanceComposerBackgroundFrost.
///
/// In en, this message translates to:
/// **'Frost blur'**
String get appearanceComposerBackgroundFrost;
/// No description provided for @appearanceGradientTitle. /// No description provided for @appearanceGradientTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+14 -1
View File
@@ -775,7 +775,20 @@ class AppLocalizationsEn extends AppLocalizations {
String get appearanceChatChromeNone => 'None'; String get appearanceChatChromeNone => 'None';
@override @override
String get appearanceChatChromeTransparent => 'Clear'; String get appearanceChatChromeTransparent => 'Frost blur';
@override
String get appearanceComposerTitle => 'Input bar';
@override
String get appearanceComposerSubtitle =>
'Style and background of the message input bar';
@override
String get appearanceComposerBackgroundStandard => 'Default';
@override
String get appearanceComposerBackgroundFrost => 'Frost blur';
@override @override
String get appearanceGradientTitle => 'Gradient'; String get appearanceGradientTitle => 'Gradient';
+13 -1
View File
@@ -778,7 +778,19 @@ class AppLocalizationsRu extends AppLocalizations {
String get appearanceChatChromeNone => 'Нет'; String get appearanceChatChromeNone => 'Нет';
@override @override
String get appearanceChatChromeTransparent => 'Прозр.'; String get appearanceChatChromeTransparent => 'Frost blur';
@override
String get appearanceComposerTitle => 'Вид панели ввода';
@override
String get appearanceComposerSubtitle => 'Стиль и фон панели ввода сообщений';
@override
String get appearanceComposerBackgroundStandard => 'Default';
@override
String get appearanceComposerBackgroundFrost => 'Frost blur';
@override @override
String get appearanceGradientTitle => 'Градиент'; String get appearanceGradientTitle => 'Градиент';
+5 -1
View File
@@ -260,7 +260,11 @@
"appearanceChatChromeColor": "Цвет", "appearanceChatChromeColor": "Цвет",
"appearanceChatChromeBlur": "Блюр", "appearanceChatChromeBlur": "Блюр",
"appearanceChatChromeNone": "Нет", "appearanceChatChromeNone": "Нет",
"appearanceChatChromeTransparent": "Прозр.", "appearanceChatChromeTransparent": "Frost blur",
"appearanceComposerTitle": "Вид панели ввода",
"appearanceComposerSubtitle": "Стиль и фон панели ввода сообщений",
"appearanceComposerBackgroundStandard": "Default",
"appearanceComposerBackgroundFrost": "Frost blur",
"appearanceGradientTitle": "Градиент", "appearanceGradientTitle": "Градиент",
"appearanceGradientSubtitle": "Объём и блики в Glossy-капсулах", "appearanceGradientSubtitle": "Объём и блики в Glossy-капсулах",
"appearanceAccentColorTitle": "Акцентный цвет", "appearanceAccentColorTitle": "Акцентный цвет",
+6
View File
@@ -38,6 +38,8 @@ import 'core/config/app_media_cache.dart';
import 'core/config/app_pill_gradient.dart'; import 'core/config/app_pill_gradient.dart';
import 'core/config/app_visual_style.dart'; import 'core/config/app_visual_style.dart';
import 'core/config/app_chat_chrome.dart'; import 'core/config/app_chat_chrome.dart';
import 'core/config/app_composer_background.dart';
import 'core/config/app_composer_style.dart';
import 'core/config/app_wallpaper_tint.dart'; import 'core/config/app_wallpaper_tint.dart';
import 'core/storage/chat_wallpaper_store.dart'; import 'core/storage/chat_wallpaper_store.dart';
import 'core/utils/wallpaper_seed.dart'; import 'core/utils/wallpaper_seed.dart';
@@ -190,6 +192,8 @@ void main(List<String> args) async {
final pillGradientFuture = AppPillGradient.load(); final pillGradientFuture = AppPillGradient.load();
final visualStyleFuture = AppVisualStyle.load(); final visualStyleFuture = AppVisualStyle.load();
final chatChromeFuture = AppChatChrome.load(); final chatChromeFuture = AppChatChrome.load();
final composerStyleFuture = AppComposerStyle.load();
final composerBackgroundFuture = AppComposerBackground.load();
final wallpaperTintFuture = AppWallpaperTint.load(); final wallpaperTintFuture = AppWallpaperTint.load();
final themeScheduleFuture = AppThemeSchedule.load(); final themeScheduleFuture = AppThemeSchedule.load();
final messageActionsFuture = AppMessageActionsStyle.load(); final messageActionsFuture = AppMessageActionsStyle.load();
@@ -241,6 +245,8 @@ void main(List<String> args) async {
pillGradientFuture, pillGradientFuture,
visualStyleFuture, visualStyleFuture,
chatChromeFuture, chatChromeFuture,
composerStyleFuture,
composerBackgroundFuture,
wallpaperTintFuture, wallpaperTintFuture,
themeScheduleFuture, themeScheduleFuture,
messageActionsFuture, messageActionsFuture,