simplified code across the app and consolidated duplicated helpers
This commit is contained in:
@@ -23,6 +23,10 @@ import 'highlighted_linkify.dart';
|
||||
|
||||
enum SearchHighlight { none, secondary, active }
|
||||
|
||||
const double _kSpacing = 3;
|
||||
const double _kTimeIconSize = 11;
|
||||
const Color _kTimeIconColor = Colors.grey;
|
||||
|
||||
class ChatBubble extends StatefulWidget {
|
||||
final bool isSender;
|
||||
final GetChatResponseObject bubbleData;
|
||||
@@ -30,11 +34,7 @@ class ChatBubble extends StatefulWidget {
|
||||
final bool isRead;
|
||||
final String? selfId;
|
||||
|
||||
final double spacing = 3;
|
||||
final double timeIconSize = 11;
|
||||
final Color timeIconColor = Colors.grey;
|
||||
|
||||
final void Function({bool renew}) refetch;
|
||||
final VoidCallback refetch;
|
||||
|
||||
final String? highlightQuery;
|
||||
final SearchHighlight matchHighlight;
|
||||
@@ -66,7 +66,9 @@ class _ChatBubbleState extends State<ChatBubble>
|
||||
Object? _messageKey;
|
||||
late Widget _messageWidget;
|
||||
|
||||
Offset _position = Offset.zero;
|
||||
// Only the translate offset changes per swipe frame; a notifier keeps those
|
||||
// frames from rebuilding the whole bubble.
|
||||
final ValueNotifier<Offset> _position = ValueNotifier(Offset.zero);
|
||||
Offset _dragStartPosition = Offset.zero;
|
||||
bool _swipeActionArmed = false;
|
||||
|
||||
@@ -90,6 +92,7 @@ class _ChatBubbleState extends State<ChatBubble>
|
||||
@override
|
||||
void dispose() {
|
||||
disposeDownloadTrigger();
|
||||
_position.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -165,19 +168,20 @@ class _ChatBubbleState extends State<ChatBubble>
|
||||
/// `TapGestureRecognizer` does not enter the gesture arena — otherwise
|
||||
/// it competes with (and blocks) the per-link `TapGestureRecognizer`s
|
||||
/// that `HighlightedLinkify` / the Markdown renderer attach to link spans.
|
||||
bool get _hasTapAction {
|
||||
bool get _hasTapAction => _poll != null || message.file != null;
|
||||
|
||||
RichObjectString? get _poll {
|
||||
final obj = message.originalData?['object'];
|
||||
if (obj?.type == RichObjectStringObjectType.talkPoll) return true;
|
||||
return message.file != null;
|
||||
return obj?.type == RichObjectStringObjectType.talkPoll ? obj : null;
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
final obj = message.originalData?['object'];
|
||||
if (obj?.type == RichObjectStringObjectType.talkPoll) {
|
||||
final obj = _poll;
|
||||
if (obj != null) {
|
||||
showChatBubblePollSheet(
|
||||
context,
|
||||
room: widget.chatData,
|
||||
pollId: int.parse(obj!.id),
|
||||
pollId: int.parse(obj.id),
|
||||
pollName: obj.name,
|
||||
refetch: widget.refetch,
|
||||
);
|
||||
@@ -257,10 +261,7 @@ class _ChatBubbleState extends State<ChatBubble>
|
||||
widget.bubbleData.timestamp * 1000,
|
||||
).formatHm(),
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
color: widget.timeIconColor,
|
||||
fontSize: widget.timeIconSize,
|
||||
),
|
||||
style: const TextStyle(color: _kTimeIconColor, fontSize: _kTimeIconSize),
|
||||
);
|
||||
|
||||
return Column(
|
||||
@@ -270,24 +271,23 @@ class _ChatBubbleState extends State<ChatBubble>
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onHorizontalDragStart: (_) => _dragStartPosition = _position,
|
||||
onHorizontalDragStart: (_) => _dragStartPosition = _position.value,
|
||||
onHorizontalDragUpdate: (details) {
|
||||
if (!widget.bubbleData.isReplyable) return;
|
||||
final dx = details.delta.dx - _dragStartPosition.dx;
|
||||
setState(() {
|
||||
_position = (_position.dx + dx).abs() > 60
|
||||
? Offset(_position.dx, 0)
|
||||
: Offset(_position.dx + dx, 0);
|
||||
});
|
||||
final current = _position.value.dx;
|
||||
_position.value = (current + dx).abs() > 60
|
||||
? Offset(current, 0)
|
||||
: Offset(current + dx, 0);
|
||||
// Beim Überqueren der Action-Schwelle einmalig Haptik feuern,
|
||||
// damit der User physisch spürt: "jetzt löst's beim Loslassen aus".
|
||||
final isArmed = _position.dx.abs() > 50;
|
||||
final isArmed = _position.value.dx.abs() > 50;
|
||||
if (isArmed && !_swipeActionArmed) Haptics.longPress();
|
||||
_swipeActionArmed = isArmed;
|
||||
},
|
||||
onHorizontalDragEnd: (_) {
|
||||
final isAction = _position.dx.abs() > 50;
|
||||
setState(() => _position = Offset.zero);
|
||||
final isAction = _position.value.dx.abs() > 50;
|
||||
_position.value = Offset.zero;
|
||||
_swipeActionArmed = false;
|
||||
if (widget.bubbleData.isReplyable && isAction) {
|
||||
context.read<ChatBloc>().setReferenceMessageId(
|
||||
@@ -298,8 +298,10 @@ class _ChatBubbleState extends State<ChatBubble>
|
||||
onLongPress: _showOptionsDialog,
|
||||
onDoubleTap: _showOptionsDialog,
|
||||
onTap: _hasTapAction ? _onTap : null,
|
||||
child: Transform.translate(
|
||||
offset: _position,
|
||||
child: ValueListenableBuilder<Offset>(
|
||||
valueListenable: _position,
|
||||
builder: (context, offset, child) =>
|
||||
Transform.translate(offset: offset, child: child),
|
||||
child: Bubble(
|
||||
style: _getStyle(),
|
||||
child: _BubbleContent(
|
||||
@@ -312,9 +314,6 @@ class _ChatBubbleState extends State<ChatBubble>
|
||||
isSender: widget.isSender,
|
||||
isRead: widget.isRead,
|
||||
selfId: widget.selfId,
|
||||
spacing: widget.spacing,
|
||||
timeIconSize: widget.timeIconSize,
|
||||
timeIconColor: widget.timeIconColor,
|
||||
showActorDisplayName: showActorDisplayName,
|
||||
showBubbleTime: showBubbleTime,
|
||||
downloadJob: downloadJob,
|
||||
@@ -343,9 +342,6 @@ class _BubbleContent extends StatelessWidget {
|
||||
final bool isSender;
|
||||
final bool isRead;
|
||||
final String? selfId;
|
||||
final double spacing;
|
||||
final double timeIconSize;
|
||||
final Color timeIconColor;
|
||||
final bool showActorDisplayName;
|
||||
final bool showBubbleTime;
|
||||
final DownloadJob? downloadJob;
|
||||
@@ -360,9 +356,6 @@ class _BubbleContent extends StatelessWidget {
|
||||
required this.isSender,
|
||||
required this.isRead,
|
||||
required this.selfId,
|
||||
required this.spacing,
|
||||
required this.timeIconSize,
|
||||
required this.timeIconColor,
|
||||
required this.showActorDisplayName,
|
||||
required this.showBubbleTime,
|
||||
required this.downloadJob,
|
||||
@@ -376,7 +369,7 @@ class _BubbleContent extends StatelessWidget {
|
||||
minWidth: showActorDisplayName
|
||||
? actorText.measuredWidth(context)
|
||||
: timeText.measuredWidth(context) +
|
||||
(isSender ? spacing + timeIconSize : 0) +
|
||||
(isSender ? _kSpacing + _kTimeIconSize : 0) +
|
||||
3,
|
||||
),
|
||||
child: Stack(
|
||||
@@ -409,11 +402,11 @@ class _BubbleContent extends StatelessWidget {
|
||||
children: [
|
||||
timeText,
|
||||
if (isSender) ...[
|
||||
SizedBox(width: spacing),
|
||||
const SizedBox(width: _kSpacing),
|
||||
Icon(
|
||||
isRead ? Icons.done_all_outlined : Icons.done_outlined,
|
||||
size: timeIconSize,
|
||||
color: timeIconColor,
|
||||
size: _kTimeIconSize,
|
||||
color: _kTimeIconColor,
|
||||
semanticLabel: isRead
|
||||
? A11yLabels.messageRead
|
||||
: A11yLabels.messageSent,
|
||||
|
||||
Reference in New Issue
Block a user