♻️ Better image loading animation and more commonly used blurhash
This commit is contained in:
@@ -24,193 +24,181 @@ class AccountNameplate extends HookConsumerWidget {
|
||||
final user = ref.watch(accountProvider(name));
|
||||
|
||||
return Container(
|
||||
decoration:
|
||||
isOutlined
|
||||
? BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
)
|
||||
: null,
|
||||
decoration: isOutlined
|
||||
? BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
)
|
||||
: null,
|
||||
margin: padding,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 0,
|
||||
color: Colors.transparent,
|
||||
child: user.when(
|
||||
data:
|
||||
(account) =>
|
||||
account.profile.background != null
|
||||
? AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background image
|
||||
Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CloudFileWidget(
|
||||
item: account.profile.background!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
data: (account) => account.profile.background != null
|
||||
? AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background image
|
||||
Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CloudFileWidget(
|
||||
item: account.profile.background!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Gradient overlay for text readability
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.8),
|
||||
Colors.black.withOpacity(0.1),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
// Gradient overlay for text readability
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.8),
|
||||
Colors.black.withOpacity(0.1),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Content positioned at the bottom
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Profile picture (equivalent to leading)
|
||||
ProfilePictureWidget(
|
||||
file: account.profile.picture,
|
||||
),
|
||||
),
|
||||
// Content positioned at the bottom
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
const SizedBox(width: 16),
|
||||
// Text content (equivalent to title and subtitle)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Profile picture (equivalent to leading)
|
||||
ProfilePictureWidget(
|
||||
fileId: account.profile.picture?.id,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Text content (equivalent to title and subtitle)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AccountName(
|
||||
account: account,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'@${account.name}',
|
||||
).textColor(Colors.white70),
|
||||
],
|
||||
AccountName(
|
||||
account: account,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'@${account.name}',
|
||||
).textColor(Colors.white70),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
decoration:
|
||||
isOutlined
|
||||
? BoxDecoration(
|
||||
border: Border.all(
|
||||
color:
|
||||
Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
decoration: isOutlined
|
||||
? BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
// Profile picture (equivalent to leading)
|
||||
ProfilePictureWidget(file: account.profile.picture),
|
||||
const SizedBox(width: 16),
|
||||
// Text content (equivalent to title and subtitle)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Profile picture (equivalent to leading)
|
||||
ProfilePictureWidget(
|
||||
fileId: account.profile.picture?.id,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Text content (equivalent to title and subtitle)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AccountName(
|
||||
account: account,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text('@${account.name}'),
|
||||
],
|
||||
),
|
||||
AccountName(
|
||||
account: account,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text('@${account.name}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Loading indicator (equivalent to leading)
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(width: 16),
|
||||
// Loading text content (equivalent to title and subtitle)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('loading').bold().tr(),
|
||||
const SizedBox(height: 4),
|
||||
const Text('...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
loading: () => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Loading indicator (equivalent to leading)
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(width: 16),
|
||||
// Loading text content (equivalent to title and subtitle)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('loading').bold().tr(),
|
||||
const SizedBox(height: 4),
|
||||
const Text('...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
error:
|
||||
(error, stackTrace) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
],
|
||||
),
|
||||
),
|
||||
error: (error, stackTrace) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Error icon (equivalent to leading)
|
||||
const Icon(Symbols.error),
|
||||
const SizedBox(width: 16),
|
||||
// Error text content (equivalent to title and subtitle)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('somethingWentWrong').tr().bold(),
|
||||
const SizedBox(height: 4),
|
||||
Text(error.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Error icon (equivalent to leading)
|
||||
const Icon(Symbols.error),
|
||||
const SizedBox(width: 16),
|
||||
// Error text content (equivalent to title and subtitle)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('somethingWentWrong').tr().bold(),
|
||||
const SizedBox(height: 4),
|
||||
Text(error.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -62,8 +62,8 @@ class AccountPickerSheet extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -74,23 +74,22 @@ class AccountPickerSheet extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
return searchResult.when(
|
||||
data:
|
||||
(accounts) => ListView.builder(
|
||||
itemCount: accounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final account = accounts[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: account.profile.picture?.id,
|
||||
),
|
||||
title: Text(account.nick),
|
||||
subtitle: Text('@${account.name}'),
|
||||
onTap: () => Navigator.of(context).pop(account),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading:
|
||||
() => const Center(child: CircularProgressIndicator()),
|
||||
data: (accounts) => ListView.builder(
|
||||
itemCount: accounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final account = accounts[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
file: account.profile.picture,
|
||||
),
|
||||
title: Text(account.nick),
|
||||
subtitle: Text('@${account.name}'),
|
||||
onTap: () => Navigator.of(context).pop(account),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -133,10 +133,9 @@ class _ExpandedSection extends StatelessWidget {
|
||||
},
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -144,8 +143,9 @@ class _ExpandedSection extends StatelessWidget {
|
||||
const Gap(4),
|
||||
Text(
|
||||
'Poll',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -160,8 +160,8 @@ class _ExpandedSection extends StatelessWidget {
|
||||
await showModalBottomSheet<SnWalletFund>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => const ComposeFundSheet(),
|
||||
builder: (context) =>
|
||||
const ComposeFundSheet(),
|
||||
);
|
||||
if (fund != null) {
|
||||
onFundSelected(fund);
|
||||
@@ -169,10 +169,9 @@ class _ExpandedSection extends StatelessWidget {
|
||||
},
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -180,8 +179,9 @@ class _ExpandedSection extends StatelessWidget {
|
||||
const Gap(4),
|
||||
Text(
|
||||
'fund'.tr(),
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -192,11 +192,8 @@ class _ExpandedSection extends StatelessWidget {
|
||||
),
|
||||
StickerPickerEmbedded(
|
||||
height: kInputDrawerExpandedHeight,
|
||||
onPick:
|
||||
(placeholder) => _insertPlaceholder(
|
||||
messageController,
|
||||
placeholder,
|
||||
),
|
||||
onPick: (placeholder) =>
|
||||
_insertPlaceholder(messageController, placeholder),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -373,15 +370,16 @@ class ChatInput extends HookConsumerWidget {
|
||||
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, -0.3),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
position:
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0, -0.3),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1.0,
|
||||
@@ -389,41 +387,40 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
chatSubscribe.isNotEmpty
|
||||
? Container(
|
||||
key: const ValueKey('typing-indicator'),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.more_horiz,
|
||||
size: 16,
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'typingHint'.plural(
|
||||
chatSubscribe.length,
|
||||
args: [
|
||||
chatSubscribe
|
||||
.map((x) => x.nick ?? x.account.nick)
|
||||
.join(', '),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('typing-indicator-none'),
|
||||
child: chatSubscribe.isNotEmpty
|
||||
? Container(
|
||||
key: const ValueKey('typing-indicator'),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.more_horiz,
|
||||
size: 16,
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'typingHint'.plural(
|
||||
chatSubscribe.length,
|
||||
args: [
|
||||
chatSubscribe
|
||||
.map((x) => x.nick ?? x.account.nick)
|
||||
.join(', '),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('typing-indicator-none'),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
@@ -445,41 +442,36 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
attachments.isNotEmpty
|
||||
? SizedBox(
|
||||
key: ValueKey('attachments-${attachments.length}'),
|
||||
height: 180,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return SizedBox(
|
||||
width: 180,
|
||||
child: AttachmentPreview(
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress:
|
||||
attachmentProgress['chat-upload']?[idx],
|
||||
onRequestUpload:
|
||||
() => onUploadAttachment(idx),
|
||||
onDelete: () => onDeleteAttachment(idx),
|
||||
onUpdate: (value) {
|
||||
attachments[idx] = value;
|
||||
onAttachmentsChanged(attachments);
|
||||
},
|
||||
onMove:
|
||||
(delta) => onMoveAttachment(idx, delta),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
).padding(vertical: 12)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('no-attachments'),
|
||||
child: attachments.isNotEmpty
|
||||
? SizedBox(
|
||||
key: ValueKey('attachments-${attachments.length}'),
|
||||
height: 180,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return SizedBox(
|
||||
width: 180,
|
||||
child: AttachmentPreview(
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress:
|
||||
attachmentProgress['chat-upload']?[idx],
|
||||
onRequestUpload: () => onUploadAttachment(idx),
|
||||
onDelete: () => onDeleteAttachment(idx),
|
||||
onUpdate: (value) {
|
||||
attachments[idx] = value;
|
||||
onAttachmentsChanged(attachments);
|
||||
},
|
||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
).padding(vertical: 12)
|
||||
: const SizedBox.shrink(key: ValueKey('no-attachments')),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -501,66 +493,62 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
selectedPoll != null
|
||||
? Container(
|
||||
key: const ValueKey('selected-poll'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.how_to_vote,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedPoll!.title ?? 'Poll',
|
||||
style: Theme.of(context).textTheme.bodySmall!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => onPollSelected(null),
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('no-selected-poll'),
|
||||
child: selectedPoll != null
|
||||
? Container(
|
||||
key: const ValueKey('selected-poll'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.how_to_vote,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedPoll!.title ?? 'Poll',
|
||||
style: Theme.of(context).textTheme.bodySmall!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => onPollSelected(null),
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-selected-poll')),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -582,93 +570,88 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
selectedFund != null
|
||||
? Container(
|
||||
key: const ValueKey('selected-fund'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
child: selectedFund != null
|
||||
? Container(
|
||||
key: const ValueKey('selected-fund'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.currency_exchange,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.currency_exchange,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (selectedFund!.message != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
selectedFund!.message!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(
|
||||
fontSize: 10,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (selectedFund!.message != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
selectedFund!.message!,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 10,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => onFundSelected(null),
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () => onFundSelected(null),
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(
|
||||
key: ValueKey('no-selected-fund'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-selected-fund')),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -692,59 +675,57 @@ class ChatInput extends HookConsumerWidget {
|
||||
},
|
||||
child:
|
||||
(messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
? Container(
|
||||
key: ValueKey(
|
||||
messageReplyingTo?.id ??
|
||||
messageForwardingTo?.id ??
|
||||
messageEditingTo?.id ??
|
||||
'action',
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
? Container(
|
||||
key: ValueKey(
|
||||
messageReplyingTo?.id ??
|
||||
messageForwardingTo?.id ??
|
||||
messageEditingTo?.id ??
|
||||
'action',
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
messageReplyingTo != null
|
||||
? Symbols.reply
|
||||
: messageForwardingTo != null
|
||||
? Symbols.forward
|
||||
: Symbols.edit,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
messageReplyingTo != null
|
||||
? Symbols.reply
|
||||
: messageForwardingTo != null
|
||||
? Symbols.forward
|
||||
: Symbols.edit,
|
||||
size: 18,
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
messageReplyingTo != null
|
||||
? 'chatReplyingTo'.tr(
|
||||
? 'chatReplyingTo'.tr(
|
||||
args: [
|
||||
messageReplyingTo
|
||||
?.sender
|
||||
@@ -753,60 +734,57 @@ class ChatInput extends HookConsumerWidget {
|
||||
'unknown'.tr(),
|
||||
],
|
||||
)
|
||||
: messageForwardingTo != null
|
||||
? 'chatForwarding'.tr()
|
||||
: 'chatEditing'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: onClear,
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6,
|
||||
left: 26,
|
||||
),
|
||||
child: Text(
|
||||
(messageReplyingTo ??
|
||||
messageForwardingTo ??
|
||||
messageEditingTo)
|
||||
?.content ??
|
||||
'chatNoContent'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
: messageForwardingTo != null
|
||||
? 'chatForwarding'.tr()
|
||||
: 'chatEditing'.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-action')),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: onClear,
|
||||
tooltip: 'clear'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6,
|
||||
left: 26,
|
||||
),
|
||||
child: Text(
|
||||
(messageReplyingTo ??
|
||||
messageForwardingTo ??
|
||||
messageEditingTo)
|
||||
?.content ??
|
||||
'chatNoContent'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-action')),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -815,25 +793,19 @@ class ChatInput extends HookConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip:
|
||||
isExpanded.value ? 'collapse'.tr() : 'more'.tr(),
|
||||
tooltip: isExpanded.value
|
||||
? 'collapse'.tr()
|
||||
: 'more'.tr(),
|
||||
icon: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
transitionBuilder:
|
||||
(child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child:
|
||||
isExpanded.value
|
||||
? const Icon(
|
||||
Symbols.close,
|
||||
key: ValueKey('close'),
|
||||
)
|
||||
: const Icon(
|
||||
Symbols.add,
|
||||
key: ValueKey('add'),
|
||||
),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: isExpanded.value
|
||||
? const Icon(
|
||||
Symbols.close,
|
||||
key: ValueKey('close'),
|
||||
)
|
||||
: const Icon(Symbols.add, key: ValueKey('add')),
|
||||
),
|
||||
onPressed: () {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
@@ -885,46 +857,43 @@ class ChatInput extends HookConsumerWidget {
|
||||
hintMaxLines: 1,
|
||||
hintText:
|
||||
(chatRoom.type == 1 && chatRoom.name == null)
|
||||
? 'chatDirectMessageHint'.tr(
|
||||
args: [
|
||||
getValidMembers(
|
||||
chatRoom.members!,
|
||||
).map((e) => e.account.nick).join(', '),
|
||||
],
|
||||
)
|
||||
: 'chatMessageHint'.tr(
|
||||
args: [chatRoom.name!],
|
||||
),
|
||||
? 'chatDirectMessageHint'.tr(
|
||||
args: [
|
||||
getValidMembers(
|
||||
chatRoom.members!,
|
||||
).map((e) => e.account.nick).join(', '),
|
||||
],
|
||||
)
|
||||
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
counterText:
|
||||
messageController.text.length > 1024
|
||||
? '${messageController.text.length}/4096'
|
||||
: null,
|
||||
counterText: messageController.text.length > 1024
|
||||
? '${messageController.text.length}/4096'
|
||||
: null,
|
||||
),
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
textInputAction:
|
||||
settings.enterToSend
|
||||
? TextInputAction.send
|
||||
: null,
|
||||
onSubmitted:
|
||||
settings.enterToSend ? (_) => send() : null,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
textInputAction: settings.enterToSend
|
||||
? TextInputAction.send
|
||||
: null,
|
||||
onSubmitted: settings.enterToSend
|
||||
? (_) => send()
|
||||
: null,
|
||||
);
|
||||
},
|
||||
suggestionsCallback: (pattern) async {
|
||||
// Only trigger on @ or :
|
||||
final atIndex = pattern.lastIndexOf('@');
|
||||
final colonIndex = pattern.lastIndexOf(':');
|
||||
final triggerIndex =
|
||||
atIndex > colonIndex ? atIndex : colonIndex;
|
||||
final triggerIndex = atIndex > colonIndex
|
||||
? atIndex
|
||||
: colonIndex;
|
||||
if (triggerIndex == -1) return [];
|
||||
final chopped = pattern.substring(triggerIndex);
|
||||
if (chopped.contains(' ')) return [];
|
||||
@@ -986,9 +955,7 @@ class ChatInput extends HookConsumerWidget {
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.image.id,
|
||||
),
|
||||
child: CloudImageWidget(file: sticker.image),
|
||||
),
|
||||
);
|
||||
break;
|
||||
@@ -1005,8 +972,9 @@ class ChatInput extends HookConsumerWidget {
|
||||
final text = messageController.text;
|
||||
final atIndex = text.lastIndexOf('@');
|
||||
final colonIndex = text.lastIndexOf(':');
|
||||
final triggerIndex =
|
||||
atIndex > colonIndex ? atIndex : colonIndex;
|
||||
final triggerIndex = atIndex > colonIndex
|
||||
? atIndex
|
||||
: colonIndex;
|
||||
if (triggerIndex == -1) return;
|
||||
final newText = text.replaceRange(
|
||||
triggerIndex,
|
||||
@@ -1053,16 +1021,15 @@ class ChatInput extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
isExpanded.value
|
||||
? _ExpandedSection(
|
||||
messageController: messageController,
|
||||
selectedPoll: selectedPoll,
|
||||
onPollSelected: onPollSelected,
|
||||
selectedFund: selectedFund,
|
||||
onFundSelected: onFundSelected,
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('collapsed')),
|
||||
child: isExpanded.value
|
||||
? _ExpandedSection(
|
||||
messageController: messageController,
|
||||
selectedPoll: selectedPoll,
|
||||
onPollSelected: onPollSelected,
|
||||
selectedFund: selectedFund,
|
||||
onFundSelected: onFundSelected,
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('collapsed')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -31,7 +31,7 @@ class MessageListTile extends StatelessWidget {
|
||||
radius: 20,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: sender.account.profile.picture?.id,
|
||||
file: sender.account.profile.picture,
|
||||
radius: 20,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -24,12 +24,11 @@ class MessageSenderInfo extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final timestamp =
|
||||
DateTime.now().difference(createdAt).inDays > 365
|
||||
? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal())
|
||||
: DateTime.now().difference(createdAt).inDays > 0
|
||||
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
|
||||
: DateFormat('HH:mm').format(createdAt.toLocal());
|
||||
final timestamp = DateTime.now().difference(createdAt).inDays > 365
|
||||
? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal())
|
||||
: DateTime.now().difference(createdAt).inDays > 0
|
||||
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
|
||||
: DateFormat('HH:mm').format(createdAt.toLocal());
|
||||
|
||||
if (isCompact) {
|
||||
return Row(
|
||||
@@ -41,7 +40,7 @@ class MessageSenderInfo extends StatelessWidget {
|
||||
AccountPfcGestureDetector(
|
||||
uname: sender.account.name,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: sender.account.profile.picture?.id,
|
||||
file: sender.account.profile.picture,
|
||||
radius: 14,
|
||||
),
|
||||
),
|
||||
@@ -69,7 +68,7 @@ class MessageSenderInfo extends StatelessWidget {
|
||||
AccountPfcGestureDetector(
|
||||
uname: sender.account.name,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: sender.account.profile.picture?.id,
|
||||
file: sender.account.profile.picture,
|
||||
radius: 14,
|
||||
),
|
||||
),
|
||||
@@ -106,7 +105,7 @@ class MessageSenderInfo extends StatelessWidget {
|
||||
AccountPfcGestureDetector(
|
||||
uname: sender.account.name,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: sender.account.profile.picture?.id,
|
||||
file: sender.account.profile.picture,
|
||||
radius: 16,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -99,15 +99,15 @@ class PublicRoomPreview extends HookConsumerWidget {
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
child: (room.type == 1 && room.picture?.id == null)
|
||||
child: (room.type == 1 && room.picture == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId: room.members!
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
files: room.members!
|
||||
.map((e) => e.account.profile.picture)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id != null
|
||||
: room.picture != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
file: room.picture,
|
||||
fallbackIcon: Symbols.chat,
|
||||
)
|
||||
: CircleAvatar(
|
||||
@@ -132,15 +132,15 @@ class PublicRoomPreview extends HookConsumerWidget {
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
child: (room.type == 1 && room.picture?.id == null)
|
||||
child: (room.type == 1 && room.picture == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId: room.members!
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
files: room.members!
|
||||
.map((e) => e.account.profile.picture)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id != null
|
||||
: room.picture != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
file: room.picture,
|
||||
fallbackIcon: Symbols.chat,
|
||||
)
|
||||
: CircleAvatar(
|
||||
|
||||
@@ -22,15 +22,13 @@ class ChatRoomAvatar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final avatarChild = (isDirect && room.picture?.id == null)
|
||||
final avatarChild = (isDirect && room.picture == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId: validMembers
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
files: validMembers.map((e) => e.account.profile.picture).toList(),
|
||||
)
|
||||
: room.picture?.id == null
|
||||
: room.picture == null
|
||||
? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase()))
|
||||
: ProfilePictureWidget(fileId: room.picture?.id);
|
||||
: ProfilePictureWidget(file: room.picture);
|
||||
|
||||
final badgeChild = Badge(
|
||||
isLabelVisible: summary.when(
|
||||
|
||||
@@ -262,10 +262,7 @@ class CheckInActivityWidget extends StatelessWidget {
|
||||
spacing: 12,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
fileId: result.account!.profile.picture?.id,
|
||||
radius: 12,
|
||||
),
|
||||
ProfilePictureWidget(file: result.account!.profile.picture, radius: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
||||
@@ -577,6 +577,9 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final String? id = file?.id ?? fileId;
|
||||
|
||||
final meta = file?.fileMeta is Map ? (file!.fileMeta as Map) : const {};
|
||||
final blurHash = meta['blur'] as String?;
|
||||
|
||||
final fallback = Icon(
|
||||
fallbackIcon ?? Symbols.account_circle,
|
||||
size: radius,
|
||||
@@ -590,6 +593,7 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
placeholder: fallback,
|
||||
content: () => UniversalImage(
|
||||
uri: '$serverUrl/drive/files/$id',
|
||||
blurHash: blurHash,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
@@ -625,14 +629,14 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class SplitAvatarWidget extends ConsumerWidget {
|
||||
final List<String?> filesId;
|
||||
final List<SnCloudFile?> files;
|
||||
final double radius;
|
||||
final IconData fallbackIcon;
|
||||
final Color? fallbackColor;
|
||||
|
||||
const SplitAvatarWidget({
|
||||
super.key,
|
||||
required this.filesId,
|
||||
required this.files,
|
||||
this.radius = 20,
|
||||
this.fallbackIcon = Symbols.account_circle,
|
||||
this.fallbackColor,
|
||||
@@ -640,17 +644,17 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (filesId.isEmpty) {
|
||||
if (files.isEmpty) {
|
||||
return ProfilePictureWidget(
|
||||
fileId: null,
|
||||
file: null,
|
||||
radius: radius,
|
||||
fallbackIcon: fallbackIcon,
|
||||
fallbackColor: fallbackColor,
|
||||
);
|
||||
}
|
||||
if (filesId.length == 1) {
|
||||
if (files.length == 1) {
|
||||
return ProfilePictureWidget(
|
||||
fileId: filesId[0],
|
||||
file: files[0],
|
||||
radius: radius,
|
||||
fallbackIcon: fallbackIcon,
|
||||
fallbackColor: fallbackColor,
|
||||
@@ -665,32 +669,32 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (filesId.length == 2)
|
||||
if (files.length == 2)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[0], ref, radius),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[1], ref, radius),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (filesId.length == 3)
|
||||
else if (files.length == 3)
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[0], ref, radius),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[1], ref, radius),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[2], ref, radius),
|
||||
child: _buildQuadrant(context, files[2], ref, radius),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -701,20 +705,10 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[0],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[1],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -723,22 +717,17 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[2],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[2], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: filesId.length > 4
|
||||
child: files.length > 4
|
||||
? Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'+${filesId.length - 3}',
|
||||
'+${files.length - 3}',
|
||||
style: TextStyle(
|
||||
fontSize: radius * 0.4,
|
||||
color: Theme.of(
|
||||
@@ -748,12 +737,7 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: _buildQuadrant(
|
||||
context,
|
||||
filesId[3],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
: _buildQuadrant(context, files[3], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -768,11 +752,11 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
|
||||
Widget _buildQuadrant(
|
||||
BuildContext context,
|
||||
String? fileId,
|
||||
SnCloudFile? file,
|
||||
WidgetRef ref,
|
||||
double radius,
|
||||
) {
|
||||
if (fileId == null) {
|
||||
if (file == null) {
|
||||
return Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
@@ -787,7 +771,7 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final uri = '$serverUrl/drive/files/$fileId';
|
||||
final uri = '$serverUrl/drive/files/${file.id}';
|
||||
|
||||
return SizedBox(
|
||||
width: radius,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class UniversalImage extends StatelessWidget {
|
||||
class UniversalImage extends HookWidget {
|
||||
final String uri;
|
||||
final String? blurHash;
|
||||
final BoxFit fit;
|
||||
@@ -27,6 +28,7 @@ class UniversalImage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loaded = useState(false);
|
||||
final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
|
||||
|
||||
if (isSvgImage) {
|
||||
@@ -35,9 +37,8 @@ class UniversalImage extends StatelessWidget {
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
placeholderBuilder:
|
||||
(BuildContext context) =>
|
||||
Center(child: CircularProgressIndicator()),
|
||||
placeholderBuilder: (BuildContext context) =>
|
||||
Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,8 +47,9 @@ class UniversalImage extends StatelessWidget {
|
||||
if (width != null && height != null && !noCacheOptimization) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
|
||||
cacheHeight =
|
||||
height != null ? (height! * devicePixelRatio).round() : null;
|
||||
cacheHeight = height != null
|
||||
? (height! * devicePixelRatio).round()
|
||||
: null;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
@@ -66,21 +68,72 @@ class UniversalImage extends StatelessWidget {
|
||||
memCacheWidth: cacheWidth,
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(value: progress.progress),
|
||||
child: AnimatedCircularProgressIndicator(
|
||||
value: progress.progress,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorWidget:
|
||||
(context, url, error) =>
|
||||
useFallbackImage
|
||||
? Image.asset(
|
||||
'assets/images/media-offline.jpg',
|
||||
fit: BoxFit.cover,
|
||||
key: Key('image-broke-$uri'),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
imageBuilder: (context, imageProvider) {
|
||||
Future(() => loaded.value = true);
|
||||
return AnimatedOpacity(
|
||||
opacity: loaded.value ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Image(
|
||||
image: imageProvider,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) => useFallbackImage
|
||||
? Image.asset(
|
||||
'assets/images/media-offline.jpg',
|
||||
fit: BoxFit.cover,
|
||||
key: Key('image-broke-$uri'),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCircularProgressIndicator extends HookWidget {
|
||||
final double? value;
|
||||
final Color? color;
|
||||
final double strokeWidth;
|
||||
final Duration duration;
|
||||
|
||||
const AnimatedCircularProgressIndicator({
|
||||
super.key,
|
||||
this.value,
|
||||
this.color,
|
||||
this.strokeWidth = 4.0,
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animationController = useAnimationController(duration: duration);
|
||||
final animation = useAnimation(
|
||||
Tween<double>(begin: 0.0, end: value ?? 0.0).animate(
|
||||
CurvedAnimation(parent: animationController, curve: Curves.linear),
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
animationController.animateTo(value ?? 0.0);
|
||||
return null;
|
||||
}, [value]);
|
||||
|
||||
return CircularProgressIndicator(
|
||||
value: animation,
|
||||
color: color,
|
||||
strokeWidth: strokeWidth,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,11 @@ class ComposeFormFields extends HookConsumerWidget {
|
||||
GestureDetector(
|
||||
onTap: onPublisherTap,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: state.currentPublisher.value?.picture?.id,
|
||||
file: state.currentPublisher.value?.picture,
|
||||
radius: 20,
|
||||
fallbackIcon:
|
||||
state.currentPublisher.value == null
|
||||
? Icons.question_mark
|
||||
: null,
|
||||
fallbackIcon: state.currentPublisher.value == null
|
||||
? Icons.question_mark
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -98,8 +97,8 @@ class ComposeFormFields extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.titleMedium,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
|
||||
// Description field
|
||||
@@ -115,8 +114,8 @@ class ComposeFormFields extends HookConsumerWidget {
|
||||
style: theme.textTheme.bodyMedium,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
|
||||
// Content field
|
||||
@@ -138,16 +137,17 @@ class ComposeFormFields extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
);
|
||||
},
|
||||
suggestionsCallback: (pattern) async {
|
||||
// Only trigger on @ or :
|
||||
final atIndex = pattern.lastIndexOf('@');
|
||||
final colonIndex = pattern.lastIndexOf(':');
|
||||
final triggerIndex =
|
||||
atIndex > colonIndex ? atIndex : colonIndex;
|
||||
final triggerIndex = atIndex > colonIndex
|
||||
? atIndex
|
||||
: colonIndex;
|
||||
if (triggerIndex == -1) return [];
|
||||
final chopped = pattern.substring(triggerIndex);
|
||||
if (chopped.contains(' ')) return [];
|
||||
@@ -202,7 +202,7 @@ class ComposeFormFields extends HookConsumerWidget {
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CloudImageWidget(fileId: sticker.image.id),
|
||||
child: CloudImageWidget(file: sticker.image),
|
||||
),
|
||||
);
|
||||
break;
|
||||
@@ -219,8 +219,9 @@ class ComposeFormFields extends HookConsumerWidget {
|
||||
final text = state.contentController.text;
|
||||
final atIndex = text.lastIndexOf('@');
|
||||
final colonIndex = text.lastIndexOf(':');
|
||||
final triggerIndex =
|
||||
atIndex > colonIndex ? atIndex : colonIndex;
|
||||
final triggerIndex = atIndex > colonIndex
|
||||
? atIndex
|
||||
: colonIndex;
|
||||
if (triggerIndex == -1) return;
|
||||
final newText = text.replaceRange(
|
||||
triggerIndex,
|
||||
@@ -281,8 +282,8 @@ class ArticleComposeFormFields extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.titleMedium,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
|
||||
// Description field
|
||||
@@ -297,8 +298,8 @@ class ArticleComposeFormFields extends StatelessWidget {
|
||||
style: theme.textTheme.bodyMedium,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
|
||||
// Content field (expanded)
|
||||
@@ -317,8 +318,8 @@ class ArticleComposeFormFields extends StatelessWidget {
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -135,10 +135,7 @@ class CompactReferencePost extends StatelessWidget {
|
||||
Widget _buildProfilePicture(BuildContext context) {
|
||||
// Handle publisher case
|
||||
if (post.publisher != null) {
|
||||
return ProfilePictureWidget(
|
||||
fileId: post.publisher!.picture?.id,
|
||||
radius: 16,
|
||||
);
|
||||
return ProfilePictureWidget(file: post.publisher!.picture, radius: 16);
|
||||
}
|
||||
// Handle actor case
|
||||
if (post.actor != null) {
|
||||
@@ -169,7 +166,7 @@ class CompactReferencePost extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: 16);
|
||||
return ProfilePictureWidget(file: null, radius: 16);
|
||||
}
|
||||
|
||||
String _getDisplayName() {
|
||||
|
||||
@@ -412,7 +412,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
fileId: currentRealm.picture?.id,
|
||||
file: currentRealm.picture,
|
||||
fallbackIcon: Symbols.workspaces,
|
||||
radius: 16,
|
||||
),
|
||||
@@ -428,7 +428,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
fileId: realm.picture?.id,
|
||||
file: realm.picture,
|
||||
fallbackIcon: Symbols.workspaces,
|
||||
radius: 16,
|
||||
),
|
||||
@@ -454,7 +454,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
)
|
||||
else
|
||||
ProfilePictureWidget(
|
||||
fileId: currentRealm.picture?.id,
|
||||
file: currentRealm.picture,
|
||||
fallbackIcon: Symbols.workspaces,
|
||||
radius: 16,
|
||||
),
|
||||
|
||||
@@ -54,7 +54,7 @@ class PostAwardSheet extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
return ProfilePictureWidget(file: null, radius: radius);
|
||||
}
|
||||
|
||||
String _getPublisherName() {
|
||||
|
||||
@@ -73,96 +73,94 @@ class PostQuickReply extends HookConsumerWidget {
|
||||
const kInputChipHeight = 54.0;
|
||||
|
||||
return publishers.when(
|
||||
data:
|
||||
(data) => Material(
|
||||
elevation: 2,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: kInputChipHeight),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
fileId: currentPublisher.value?.picture?.id,
|
||||
radius: (kInputChipHeight * 0.5) - 6,
|
||||
data: (data) => Material(
|
||||
elevation: 2,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: kInputChipHeight),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
file: currentPublisher.value?.picture,
|
||||
radius: (kInputChipHeight * 0.5) - 6,
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => PublisherModal(),
|
||||
).then((value) {
|
||||
if (value is SnPublisher) {
|
||||
currentPublisher.value = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
).padding(right: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: contentController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postReplyPlaceholder'.tr(),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 14,
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => PublisherModal(),
|
||||
).then((value) {
|
||||
if (value is SnPublisher) {
|
||||
currentPublisher.value = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
).padding(right: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: contentController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postReplyPlaceholder'.tr(),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 14,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
style: TextStyle(fontSize: 14),
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
onLaunch?.call();
|
||||
final value = await PostComposeSheet.show(
|
||||
context,
|
||||
initialState: PostComposeInitialState(
|
||||
content: contentController.text,
|
||||
replyingTo: parent,
|
||||
),
|
||||
);
|
||||
if (value != null) onPosted?.call();
|
||||
},
|
||||
icon: const Icon(Symbols.launch, size: 20),
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: kInputChipHeight - 6,
|
||||
minHeight: kInputChipHeight - 6,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon:
|
||||
submitting.value
|
||||
? SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
)
|
||||
: Icon(Symbols.send, size: 20),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: kInputChipHeight - 6,
|
||||
minHeight: kInputChipHeight - 6,
|
||||
),
|
||||
),
|
||||
],
|
||||
style: TextStyle(fontSize: 14),
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
onLaunch?.call();
|
||||
final value = await PostComposeSheet.show(
|
||||
context,
|
||||
initialState: PostComposeInitialState(
|
||||
content: contentController.text,
|
||||
replyingTo: parent,
|
||||
),
|
||||
);
|
||||
if (value != null) onPosted?.call();
|
||||
},
|
||||
icon: const Icon(Symbols.launch, size: 20),
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: kInputChipHeight - 6,
|
||||
minHeight: kInputChipHeight - 6,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: submitting.value
|
||||
? SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
)
|
||||
: Icon(Symbols.send, size: 20),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: kInputChipHeight - 6,
|
||||
minHeight: kInputChipHeight - 6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (e, _) => const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
@@ -148,7 +148,7 @@ class PostReplyPreview extends HookConsumerWidget {
|
||||
return ActorPictureWidget(actor: post.actor!, radius: radius);
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
return ProfilePictureWidget(file: null, radius: radius);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -448,7 +448,7 @@ class ReferencedPostWidget extends StatelessWidget {
|
||||
// Handle publisher case
|
||||
if (post.publisher != null) {
|
||||
return ProfilePictureWidget(
|
||||
fileId: post.publisher!.picture?.id,
|
||||
file: post.publisher!.picture,
|
||||
radius: radius,
|
||||
);
|
||||
}
|
||||
@@ -457,7 +457,7 @@ class ReferencedPostWidget extends StatelessWidget {
|
||||
return ActorPictureWidget(actor: post.actor!, radius: radius);
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
return ProfilePictureWidget(file: null, radius: radius);
|
||||
}
|
||||
|
||||
String _getDisplayName(SnPost post) {
|
||||
@@ -701,7 +701,7 @@ class PostHeader extends HookConsumerWidget {
|
||||
return ActorPictureWidget(actor: post.actor!, radius: radius);
|
||||
}
|
||||
// Fallback
|
||||
return ProfilePictureWidget(fileId: null, radius: radius);
|
||||
return ProfilePictureWidget(file: null, radius: radius);
|
||||
}
|
||||
|
||||
String _getDisplayName(SnPost post) {
|
||||
|
||||
@@ -21,63 +21,57 @@ class PublisherModal extends HookConsumerWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: publishers.when(
|
||||
data:
|
||||
(value) =>
|
||||
value.isEmpty
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 280),
|
||||
child:
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'publishersEmpty',
|
||||
textAlign: TextAlign.center,
|
||||
).tr().fontSize(17).bold(),
|
||||
Text(
|
||||
'publishersEmptyDescription',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
const Gap(12),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) =>
|
||||
const NewPublisherScreen(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
publishersManagedProvider,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Text('createPublisher').tr(),
|
||||
),
|
||||
],
|
||||
).center(),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
for (final publisher in value)
|
||||
ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: publisher.picture?.id,
|
||||
),
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
onTap: () {
|
||||
Navigator.pop(context, publisher);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
data: (value) => value.isEmpty
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 280),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'publishersEmpty',
|
||||
textAlign: TextAlign.center,
|
||||
).tr().fontSize(17).bold(),
|
||||
Text(
|
||||
'publishersEmptyDescription',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
const Gap(12),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) =>
|
||||
const NewPublisherScreen(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Text('createPublisher').tr(),
|
||||
),
|
||||
],
|
||||
).center(),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
for (final publisher in value)
|
||||
ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
file: publisher.picture,
|
||||
),
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
onTap: () {
|
||||
Navigator.pop(context, publisher);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Text('Error: $e'),
|
||||
),
|
||||
|
||||
@@ -30,17 +30,16 @@ class RealmListTile extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child:
|
||||
realm.background == null
|
||||
? const SizedBox.shrink()
|
||||
: CloudImageWidget(file: realm.background),
|
||||
child: realm.background == null
|
||||
? const SizedBox.shrink()
|
||||
: CloudImageWidget(file: realm.background),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -30,
|
||||
left: 18,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: realm.picture?.id,
|
||||
file: realm.picture,
|
||||
fallbackIcon: Symbols.group,
|
||||
radius: 24,
|
||||
),
|
||||
|
||||
@@ -49,7 +49,7 @@ class RealmSelectionDropdown extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
fileId: realm.picture?.id,
|
||||
file: realm.picture,
|
||||
fallbackIcon: Symbols.workspaces,
|
||||
radius: 16,
|
||||
),
|
||||
|
||||
@@ -776,19 +776,19 @@ class _ChatRoomOption extends HookConsumerWidget {
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: (isDirect && room.picture?.id == null)
|
||||
child: (isDirect && room.picture == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId: validMembers
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
files: validMembers
|
||||
.map((e) => e.account.profile.picture)
|
||||
.toList(),
|
||||
radius: 16,
|
||||
)
|
||||
: room.picture?.id == null
|
||||
: room.picture == null
|
||||
? CircleAvatar(
|
||||
radius: 16,
|
||||
child: Text(room.name![0].toUpperCase()),
|
||||
)
|
||||
: ProfilePictureWidget(fileId: room.picture?.id, radius: 16),
|
||||
: ProfilePictureWidget(file: room.picture, radius: 16),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Chat room name
|
||||
|
||||
@@ -75,31 +75,29 @@ class StickerPicker extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
loading:
|
||||
() => const SizedBox(
|
||||
width: 320,
|
||||
height: 320,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error:
|
||||
(err, _) => SizedBox(
|
||||
width: 360,
|
||||
height: 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.error, size: 28),
|
||||
const Gap(8),
|
||||
Text('Error: $err', textAlign: TextAlign.center),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
loading: () => const SizedBox(
|
||||
width: 320,
|
||||
height: 320,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (err, _) => SizedBox(
|
||||
width: 360,
|
||||
height: 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.error, size: 28),
|
||||
const Gap(8),
|
||||
Text('Error: $err', textAlign: TextAlign.center),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -263,7 +261,7 @@ class _StickersGrid extends StatelessWidget {
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.image.id,
|
||||
file: sticker.image,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
@@ -310,31 +308,29 @@ class StickerPickerEmbedded extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
loading:
|
||||
() => SizedBox(
|
||||
width: 320,
|
||||
height: height ?? 320,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error:
|
||||
(err, _) => SizedBox(
|
||||
width: 360,
|
||||
height: height ?? 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.error, size: 28),
|
||||
const Gap(8),
|
||||
Text('Error: $err', textAlign: TextAlign.center),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
loading: () => SizedBox(
|
||||
width: 320,
|
||||
height: height ?? 320,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (err, _) => SizedBox(
|
||||
width: 360,
|
||||
height: height ?? 200,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.error, size: 28),
|
||||
const Gap(8),
|
||||
Text('Error: $err', textAlign: TextAlign.center),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -386,18 +382,16 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
selected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border:
|
||||
selected
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 4,
|
||||
)
|
||||
: null,
|
||||
border: selected
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 4,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: InkWell(
|
||||
@@ -413,11 +407,11 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
|
||||
builder: (context, value, _) {
|
||||
return packs[i].icon != null
|
||||
? CloudImageWidget(
|
||||
file: packs[i].icon!,
|
||||
).clipRRect(all: value)
|
||||
file: packs[i].icon!,
|
||||
).clipRRect(all: value)
|
||||
: CloudImageWidget(
|
||||
file: packs[i].stickers.firstOrNull?.image,
|
||||
).clipRRect(all: value);
|
||||
file: packs[i].stickers.firstOrNull?.image,
|
||||
).clipRRect(all: value);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -458,18 +452,17 @@ Future<void> showStickerPickerPopover(
|
||||
offset: offset,
|
||||
alignment: alignment ?? Alignment.topLeft,
|
||||
dimBackground: true,
|
||||
builder:
|
||||
(ctx) => SizedBox(
|
||||
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
|
||||
height: 480,
|
||||
child: ProviderScope(
|
||||
child: StickerPicker(
|
||||
onPick: (ph) {
|
||||
onPick(ph);
|
||||
Navigator.of(ctx).maybePop();
|
||||
},
|
||||
),
|
||||
),
|
||||
builder: (ctx) => SizedBox(
|
||||
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
|
||||
height: 480,
|
||||
child: ProviderScope(
|
||||
child: StickerPicker(
|
||||
onPick: (ph) {
|
||||
onPick(ph);
|
||||
Navigator.of(ctx).maybePop();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user