Skip to content
28 changes: 23 additions & 5 deletions mobile/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,25 @@ class App extends HookConsumerWidget {
final schemeName = ref.watch(schemeProvider);
final authState = ref.watch(authProvider);

final resolved = resolveSchemes(schemeName);
final resolved = resolveSchemes(schemeName, themeMode);
final lightScheme = applyAccent(resolved.light, accentIndex);
final darkScheme = applyAccent(resolved.dark, accentIndex);
// Default and named schemes can force light or dark mode; otherwise
// respect the user's ThemeMode preference.
// Light/Dark modes pin the brightness; System leaves it null so Flutter
// follows the OS across the selected theme and its pair.
final effectiveMode = resolved.forcedMode ?? themeMode;

// Derive the gradient from the themes that produced each color scheme.
// This keeps fallbacks and pinned brightness changes aligned with the
// rendered palette rather than the raw persisted selection.
final buzzLightGradient = buzzTopSectionGradient(
resolved.lightTheme?.name ?? '',
lightScheme.brightness,
);
final buzzDarkGradient = buzzTopSectionGradient(
resolved.darkTheme?.name ?? '',
darkScheme.brightness,
);

// Eagerly initialize websocket session and lifecycle observer when
// authenticated. These providers connect and manage the websocket.
if (authState.value?.status == AuthStatus.authenticated) {
Expand Down Expand Up @@ -65,8 +77,14 @@ class App extends HookConsumerWidget {

return MaterialApp(
title: 'Buzz',
theme: AppTheme.light(colorScheme: lightScheme),
darkTheme: AppTheme.dark(colorScheme: darkScheme),
theme: AppTheme.light(
colorScheme: lightScheme,
topSectionGradient: buzzLightGradient,
),
darkTheme: AppTheme.dark(
colorScheme: darkScheme,
topSectionGradient: buzzDarkGradient,
),
themeMode: effectiveMode,
home: authState.when(
loading: () => const _SplashScreen(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../shared/auth/auth.dart';
import '../../shared/custom_emoji/custom_emoji.dart';
import '../../shared/custom_emoji/custom_emoji_provider.dart';
import '../../shared/relay/relay.dart';
import '../profile/profile_provider.dart';
import '../custom_emoji/custom_emoji.dart';
import '../custom_emoji/custom_emoji_provider.dart';
import 'channel.dart';
import 'channels_provider.dart';

Expand Down
31 changes: 26 additions & 5 deletions mobile/lib/features/channels/channels_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import '../../shared/theme/theme.dart';
import '../../shared/widgets/avatar_image.dart';
import '../../shared/widgets/frosted_app_bar.dart';
import '../../shared/widgets/frosted_scaffold.dart';
import '../custom_emoji/custom_emoji.dart';
import '../custom_emoji/custom_emoji_provider.dart';
import '../custom_emoji/custom_emoji_render.dart';
import '../../shared/custom_emoji/custom_emoji.dart';
import '../../shared/custom_emoji/custom_emoji_provider.dart';
import '../../shared/custom_emoji/custom_emoji_render.dart';
import '../profile/profile_avatar.dart';
import '../profile/profile_provider.dart';
import '../profile/settings_profile_header.dart';
Comment thread
klopez4212 marked this conversation as resolved.
Outdated
import '../settings/settings_page.dart';
import '../profile/presence_cache_provider.dart';
import '../profile/user_cache_provider.dart';
Expand Down Expand Up @@ -62,6 +63,19 @@ const double _kChannelLabelGap = Grid.xxs;
const double _kChannelRowVerticalPadding = Grid.xxs + Grid.quarter;
const double _kChannelLabelInset =
_kChannelSectionInset + _kChannelLeadingWidth + _kChannelLabelGap;

/// DM avatars are circles, so they fill their box edge to edge where a channel
/// glyph leaves 4dp of slack inside the same 22dp leading column. Sizing them to
/// the glyph's ink width keeps the icon-to-label distance identical across both
/// sections while the labels stay on [_kChannelLabelInset].
const double _kDmAvatarSize = _kChannelIconSize;

/// The top section's avatars are 32dp circles, which fill their box edge to
/// edge; the channel rows below lead with an 18dp glyph left-aligned in a 22dp
/// box at [_kChannelSectionInset]. Edge-aligning the two leaves the circles
/// looking pushed outward, so the bar is pulled in to sit the avatar's centre
/// on the channel-icon column (12 + 16 = 28dp against the glyph's ~29dp).
const double _kTopSectionInset = Grid.twelve;
const Duration _kSectionExpandDuration = Duration(milliseconds: 220);
const Duration _kSectionCollapseDuration = Duration(milliseconds: 170);
const Curve _kSectionExpandCurve = Cubic(0.23, 1, 0.32, 1);
Expand Down Expand Up @@ -208,7 +222,11 @@ class ChannelsPage extends HookConsumerWidget {

return FrostedScaffold(
appBar: FrostedAppBar(
horizontalInset: _kChannelSectionInset,
horizontalInset: _kTopSectionInset,
// Under a Buzz theme the community + account avatar strip carries the
// branded gradient, the way desktop paints it across the sidebar. Null
// under every other theme, leaving the default frosted fill.
gradient: context.appColors.topSectionGradient,
leading: _CommunityIndicator(
onTap: () => showModalBottomSheet<void>(
context: context,
Expand All @@ -220,7 +238,10 @@ class ChannelsPage extends HookConsumerWidget {
actions: [
ProfileAvatar(
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const SettingsPage()),
MaterialPageRoute<void>(
builder: (_) =>
const SettingsPage(profileHeader: SettingsProfileHeader()),
),
),
),
],
Expand Down
41 changes: 20 additions & 21 deletions mobile/lib/features/channels/channels_page/channel_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,19 @@ class _ChannelTile extends ConsumerWidget {
),
child: Row(
children: [
if (channel.isDm)
_DmAvatar(channel: channel, currentPubkey: currentPubkey)
else
SizedBox(
width: _kChannelLeadingWidth,
child: Align(
alignment: Alignment.centerLeft,
child: Icon(
channelIcon(channel),
size: _kChannelIconSize,
color: context.colors.onSurface,
),
),
SizedBox(
width: _kChannelLeadingWidth,
child: Align(
alignment: Alignment.centerLeft,
child: channel.isDm
? _DmAvatar(channel: channel, currentPubkey: currentPubkey)
: Icon(
channelIcon(channel),
size: _kChannelIconSize,
color: context.colors.onSurface,
),
),
),
const SizedBox(width: _kChannelLabelGap),
Expanded(
child: Column(
Expand Down Expand Up @@ -343,8 +342,8 @@ class _DmAvatar extends ConsumerWidget {

if (visiblePubkeys.length > 1) {
return Container(
width: 22,
height: 22,
width: _kDmAvatarSize,
height: _kDmAvatarSize,
alignment: Alignment.center,
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest,
Expand All @@ -354,7 +353,7 @@ class _DmAvatar extends ConsumerWidget {
child: Text(
'${visiblePubkeys.length}',
style: context.textTheme.labelSmall?.copyWith(
fontSize: 10,
fontSize: 9,
color: context.colors.onSurface,
fontWeight: FontWeight.w600,
height: 1,
Expand Down Expand Up @@ -385,14 +384,14 @@ class _DmAvatar extends ConsumerWidget {
: 'offline';

return SizedBox(
width: 22,
height: 22,
width: _kDmAvatarSize,
height: _kDmAvatarSize,
child: Stack(
clipBehavior: Clip.none,
children: [
AvatarImage(
imageUrl: avatarUrl,
radius: 10,
radius: _kDmAvatarSize / 2,
backgroundColor: context.colors.primaryContainer,
fallback: Text(
initial,
Expand All @@ -407,8 +406,8 @@ class _DmAvatar extends ConsumerWidget {
right: -1,
bottom: -1,
child: Container(
width: 9,
height: 9,
width: 8,
height: 8,
decoration: BoxDecoration(
color: _presenceColor(context, presence),
shape: BoxShape.circle,
Expand Down
4 changes: 2 additions & 2 deletions mobile/lib/features/channels/compose_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import '../../shared/theme/theme.dart';
import '../../shared/widgets/avatar_image.dart';
import '../profile/user_cache_provider.dart';
import '../profile/user_profile.dart';
import '../custom_emoji/custom_emoji.dart';
import '../custom_emoji/custom_emoji_provider.dart';
import '../../shared/custom_emoji/custom_emoji.dart';
import '../../shared/custom_emoji/custom_emoji_provider.dart';
import 'camera_capture_cleanup.dart';
import 'channel.dart';
import 'channel_management_provider.dart';
Expand Down
6 changes: 3 additions & 3 deletions mobile/lib/features/channels/emoji_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';

import '../../shared/theme/theme.dart';
import '../custom_emoji/custom_emoji.dart';
import '../custom_emoji/custom_emoji_provider.dart';
import '../custom_emoji/custom_emoji_render.dart';
import '../../shared/custom_emoji/custom_emoji.dart';
import '../../shared/custom_emoji/custom_emoji_provider.dart';
import '../../shared/custom_emoji/custom_emoji_render.dart';

/// Opens the full emoji picker as a modal bottom sheet.
void showEmojiPicker({
Expand Down
4 changes: 2 additions & 2 deletions mobile/lib/features/channels/message_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';

import '../../shared/theme/theme.dart';
import '../custom_emoji/custom_emoji.dart';
import '../custom_emoji/custom_emoji_provider.dart';
import '../../shared/custom_emoji/custom_emoji.dart';
import '../../shared/custom_emoji/custom_emoji_provider.dart';
import 'channel_management_provider.dart';
import 'emoji_picker.dart';
import 'thread_detail_page.dart';
Expand Down
6 changes: 3 additions & 3 deletions mobile/lib/features/channels/message_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import '../../shared/clipboard_utils.dart';
import '../../shared/relay/relay.dart';
import '../../shared/syntax_highlight.dart';
import '../../shared/theme/theme.dart';
import '../custom_emoji/custom_emoji.dart';
import '../custom_emoji/custom_emoji_provider.dart';
import '../custom_emoji/custom_emoji_render.dart';
import '../../shared/custom_emoji/custom_emoji.dart';
import '../../shared/custom_emoji/custom_emoji_provider.dart';
import '../../shared/custom_emoji/custom_emoji_render.dart';
import 'media_viewer_page.dart';
import 'message_media.dart';

Expand Down
2 changes: 1 addition & 1 deletion mobile/lib/features/channels/reaction_row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../shared/theme/theme.dart';
import '../../shared/widgets/avatar_image.dart';
import '../custom_emoji/custom_emoji_render.dart';
import '../../shared/custom_emoji/custom_emoji_render.dart';
import '../profile/user_cache_provider.dart';
import '../profile/user_profile.dart';
import 'channel_management_provider.dart';
Expand Down
2 changes: 1 addition & 1 deletion mobile/lib/features/channels/timeline_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';

import '../../shared/relay/relay.dart';
import '../custom_emoji/custom_emoji.dart';
import '../../shared/custom_emoji/custom_emoji.dart';
import 'channel_window.dart';

enum SystemEventType {
Expand Down
4 changes: 2 additions & 2 deletions mobile/lib/features/forum/forum_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../shared/relay/relay.dart';
import '../channels/channel_management_provider.dart';
import '../custom_emoji/custom_emoji.dart';
import '../custom_emoji/custom_emoji_provider.dart';
import '../../shared/custom_emoji/custom_emoji.dart';
import '../../shared/custom_emoji/custom_emoji_provider.dart';
import 'forum_models.dart';

/// Fetches forum posts (kind:45001) for a channel from the relay.
Expand Down
67 changes: 40 additions & 27 deletions mobile/lib/features/profile/profile_avatar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,23 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../shared/theme/theme.dart';
import '../../shared/widgets/avatar_image.dart';
import '../../shared/widgets/masked_avatar_badge.dart';
import 'profile_provider.dart';
import 'user_profile.dart';

/// Matches desktop's sidebar profile card, whose avatar is 32px.
const _avatarSize = 32.0;

/// The visible dot is smaller than the notch it sits in, so a ring of
/// background separates it from the avatar. Desktop's `h-2 w-2` dot inside a
/// `h-3.5 w-3.5` badge frame.
const _presenceDotRatio = 8 / 14;

/// User avatar with a presence dot indicator, for use in the app bar.
///
/// The dot sits in a notch masked out of the avatar rather than overlapping it,
/// the same treatment desktop's `SidebarProfileCard` uses — so it needs no
/// background-coloured ring, and reads correctly over the themed top section.
class ProfileAvatar extends ConsumerWidget {
final VoidCallback? onTap;
final bool showPresence;
Expand All @@ -32,7 +45,7 @@ class ProfileAvatar extends ConsumerWidget {

Widget _buildPlaceholder(BuildContext context) {
return CircleAvatar(
radius: 16,
radius: _avatarSize / 2,
backgroundColor: context.colors.primaryContainer,
);
}
Expand All @@ -44,37 +57,37 @@ class ProfileAvatar extends ConsumerWidget {
) {
return GestureDetector(
onTap: onTap,
child: Stack(
children: [
AvatarImage(
imageUrl: profile?.avatarUrl,
radius: 16,
backgroundColor: context.colors.primaryContainer,
fallback: Text(
profile?.initial ?? '?',
style: context.textTheme.labelMedium?.copyWith(
color: context.colors.onPrimaryContainer,
child: MaskedAvatarBadge(
size: _avatarSize,
geometry: AvatarBadgeMaskGeometry.presenceDot,
avatar: ClipOval(
child: ColoredBox(
color: context.colors.primaryContainer,
child: AvatarImageContent(
imageUrl: profile?.avatarUrl,
fallback: Text(
profile?.initial ?? '?',
style: context.textTheme.labelMedium?.copyWith(
color: context.colors.onPrimaryContainer,
),
),
),
),
if (showPresence)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: _presenceColor(context, presence),
shape: BoxShape.circle,
border: Border.all(
color: context.theme.scaffoldBackgroundColor,
width: 1.5,
),
badge: showPresence
? Center(
child: FractionallySizedBox(
widthFactor: _presenceDotRatio,
heightFactor: _presenceDotRatio,
child: DecoratedBox(
decoration: BoxDecoration(
color: _presenceColor(context, presence),
shape: BoxShape.circle,
),
),
),
),
),
],
)
: null,
),
);
}
Expand Down
Loading
Loading