mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
## Summary Refresh mobile conversations with left-aligned grouped bubbles without tails, tighter consecutive-message spacing, distinct sender colors, theme-aware incoming backgrounds, and compact overlapping reaction trays. Attach reply counts beside reactions with up to three participant avatars; keep outgoing mentions and links readable in both themes. Coordinate a 500 ms composer-to-message transition with spring contraction and an anchored avatar. Message ownership follows the signing identity so profile reloads cannot recolor outgoing bubbles. Center channel activities without timestamps or icons, keep names tappable, and show two affected members plus a **See all** sheet. Hide legacy purpose activities, match media corners to bubble groups, and add iOS status-bar scroll-to-top. This is an independent mobile PR; no desktop changes or color picker. ## Validation - Full `just mobile-check` passed; `just mobile-test` passed 3,275 tests with 4 skips, plus 3 unconfigured-gateway checks. - Fixed message-widget lint warnings and a stale notification fixture that blocked analysis. Made the video-controller disposal test wait for lifecycle events instead of fixed delays. Mobile pre-push checks are enabled. - Physical iPhone previews were installed throughout development. The latest bubble, reply-avatar, contrast, and spacing updates were installed and launched on the physical iPhone. Final motion and status-bar behavior still need device visual verification; Android has automated coverage but no physical-device verification. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
130 lines
3.9 KiB
Dart
130 lines
3.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/theme.dart';
|
|
|
|
/// A consistent inline author row for message-oriented surfaces.
|
|
class MessageAuthorMeta extends StatelessWidget {
|
|
/// Primary author name shown at the start of the row.
|
|
final String displayName;
|
|
|
|
/// Optional secondary username, hidden when blank or equal to [displayName].
|
|
final String? username;
|
|
|
|
/// Timestamp label shown after the author metadata.
|
|
final String timestamp;
|
|
|
|
/// Whether metadata includes the timestamp alongside the author.
|
|
final bool showTimestamp;
|
|
|
|
/// Color applied to [displayName].
|
|
final Color nameColor;
|
|
|
|
/// Color applied to the username and [timestamp].
|
|
final Color metadataColor;
|
|
|
|
/// Optional callback invoked when [displayName] is tapped.
|
|
final VoidCallback? onAuthorTap;
|
|
|
|
/// Optional key assigned to the display-name text.
|
|
final Key? displayNameKey;
|
|
|
|
/// Optional key assigned to the username text.
|
|
final Key? usernameKey;
|
|
|
|
/// Optional key assigned to the timestamp text.
|
|
final Key? timestampKey;
|
|
|
|
/// Base text style for [displayName], with [nameColor] applied.
|
|
final TextStyle nameStyle;
|
|
|
|
/// Base text style for secondary metadata, with [metadataColor] applied.
|
|
final TextStyle metadataStyle;
|
|
|
|
/// Base text style for [timestamp], with [metadataColor] applied.
|
|
final TextStyle timestampStyle;
|
|
|
|
/// Creates an inline author row with optional username and tap handling.
|
|
const MessageAuthorMeta({
|
|
super.key,
|
|
required this.displayName,
|
|
required this.timestamp,
|
|
required this.nameColor,
|
|
required this.metadataColor,
|
|
this.username,
|
|
this.showTimestamp = true,
|
|
this.onAuthorTap,
|
|
this.displayNameKey,
|
|
this.usernameKey,
|
|
this.timestampKey,
|
|
this.nameStyle = messageUsernameTextStyle,
|
|
this.metadataStyle = messageMetadataTextStyle,
|
|
this.timestampStyle = messageTimestampTextStyle,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final normalizedUsername = username?.trim();
|
|
final showUsername =
|
|
normalizedUsername != null &&
|
|
normalizedUsername.isNotEmpty &&
|
|
normalizedUsername != displayName.trim();
|
|
final resolvedNameStyle = nameStyle.copyWith(color: nameColor);
|
|
final resolvedMetadataStyle = metadataStyle.copyWith(color: metadataColor);
|
|
final resolvedTimestampStyle = timestampStyle.copyWith(
|
|
color: metadataColor,
|
|
);
|
|
|
|
Widget authorName = Text(
|
|
displayName,
|
|
key: displayNameKey,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: resolvedNameStyle,
|
|
);
|
|
if (onAuthorTap != null) {
|
|
authorName = GestureDetector(onTap: onAuthorTap, child: authorName);
|
|
}
|
|
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final metadataMaxWidth = constraints.hasBoundedWidth
|
|
? constraints.maxWidth / (showUsername ? 3 : 2)
|
|
: double.infinity;
|
|
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Flexible(child: authorName),
|
|
if (showUsername) ...[
|
|
const SizedBox(width: Grid.half),
|
|
ConstrainedBox(
|
|
constraints: BoxConstraints(maxWidth: metadataMaxWidth),
|
|
child: Text(
|
|
normalizedUsername,
|
|
key: usernameKey,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: resolvedMetadataStyle,
|
|
),
|
|
),
|
|
],
|
|
if (showTimestamp) ...[
|
|
const SizedBox(width: Grid.xxs),
|
|
ConstrainedBox(
|
|
constraints: BoxConstraints(maxWidth: metadataMaxWidth),
|
|
child: Text(
|
|
timestamp,
|
|
key: timestampKey,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: resolvedTimestampStyle,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|