feat: cartões de publicação em canais e corrreções de salas arquivadas
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled

- Canais: publicações viram cartões de largura total mantendo todas as
  funções do balão (reações, resposta, editar, excluir, comentários em
  thread). Composer de publicação fica restrito a admins no feed; membros
  comentam via tela de thread. Canais novos usam events_default 0 para
  permitir comentários e reações.
- Ao sair/excluir uma sala, a cópia local é removida do banco imediatamente;
  salas já deixadas no servidor não apresentam mais erro ao excluir de novo.
- Classificação mural/canal carregada direto do sync (room_type em
  importantStateEvents + postLoad) sem precisar abrir a sala.
- Diálogos com raio e padding reduzidos (dialogTheme, text input e ok/cancel).
- Ícones do app regenerados (logo_background) e dependência linkify.
This commit is contained in:
2026-09-14 08:30:25 -03:00
parent 41a6a52393
commit ebafa1dc39
29 changed files with 10543 additions and 9965 deletions

View File

@@ -55,6 +55,12 @@ class Message extends StatelessWidget {
final bool isCollapsed;
final Set<String> bigEmojis;
/// Renders the message as a full-width publication card (used in canal
/// rooms). Keeps every message feature (reply, reactions, edit, delete,
/// thread…), but drops the left/right bubble layout and stretches the
/// "bubble" over the whole available width.
final bool fullWidthCard;
const Message(
this.event, {
this.nextEvent,
@@ -79,6 +85,7 @@ class Message extends StatelessWidget {
this.onExpand,
required this.enterThread,
this.isCollapsed = false,
this.fullWidthCard = false,
super.key,
});
@@ -105,7 +112,12 @@ class Message extends StatelessWidget {
final client = Matrix.of(context).client;
final ownMessage = event.senderId == client.userID;
final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft;
final card = fullWidthCard;
final alignment = card
? Alignment.topLeft
: ownMessage
? Alignment.topRight
: Alignment.topLeft;
var color = theme.colorScheme.surfaceContainerHigh;
final displayTime =
@@ -128,6 +140,10 @@ class Message extends StatelessWidget {
previousEvent!.senderId == event.senderId &&
previousEvent!.originServerTs.sameEnvironment(event.originServerTs);
// In card mode the sender name moves inside the card, so show it even for
// own messages (otherwise a publication feed would be anonymous).
final showSenderName = !nextEventSameSender && (card || !ownMessage);
final textColor = ownMessage
? theme.onBubbleColor
: theme.colorScheme.onSurface;
@@ -136,21 +152,27 @@ class Message extends StatelessWidget {
? theme.colorScheme.onTertiaryContainer
: theme.colorScheme.tertiary;
final rowMainAxisAlignment = ownMessage
final rowMainAxisAlignment = card
? MainAxisAlignment.start
: ownMessage
? MainAxisAlignment.end
: MainAxisAlignment.start;
final displayEvent = event.getDisplayEvent(timeline);
const hardCorner = Radius.circular(3);
const roundedCorner = Radius.circular(AppConfig.borderRadius);
final borderRadius = BorderRadius.only(
topLeft: !ownMessage ? hardCorner : roundedCorner,
topRight: ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
bottomLeft: !ownMessage && previousEventSameSender
? hardCorner
: roundedCorner,
bottomRight: ownMessage ? hardCorner : roundedCorner,
);
final borderRadius = card
? BorderRadius.all(roundedCorner)
: BorderRadius.only(
topLeft: !ownMessage ? hardCorner : roundedCorner,
topRight: ownMessage && nextEventSameSender
? hardCorner
: roundedCorner,
bottomLeft: !ownMessage && previousEventSameSender
? hardCorner
: roundedCorner,
bottomRight: ownMessage ? hardCorner : roundedCorner,
);
const avatarSize = Avatar.defaultSize;
final noBubble =
({
@@ -229,9 +251,9 @@ class Message extends StatelessWidget {
: SwipeDirection.startToEnd,
onSwipe: (_) => onSwipe(),
child: Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.maxTimelineWidth,
),
constraints: card
? const BoxConstraints()
: const BoxConstraints(maxWidth: FluffyThemes.maxTimelineWidth),
padding: EdgeInsets.only(
left: 8.0,
right: 8.0,
@@ -242,7 +264,11 @@ class Message extends StatelessWidget {
),
child: Column(
mainAxisSize: .min,
crossAxisAlignment: ownMessage ? .end : .start,
crossAxisAlignment: card
? CrossAxisAlignment.stretch
: ownMessage
? .end
: .start,
children: <Widget>[
Stack(
clipBehavior: Clip.none,
@@ -275,7 +301,9 @@ class Message extends StatelessWidget {
crossAxisAlignment: .start,
mainAxisAlignment: rowMainAxisAlignment,
children: [
if (longPressSelect && !event.redacted)
if (card)
const SizedBox.shrink()
else if (longPressSelect && !event.redacted)
SizedBox(
height: avatarSize,
width: avatarSize,
@@ -318,70 +346,76 @@ class Message extends StatelessWidget {
crossAxisAlignment: .start,
mainAxisSize: .min,
children: [
Padding(
padding: EdgeInsets.only(left: 8.0),
child: Row(
mainAxisAlignment: ownMessage ? .end : .start,
children: [
if (sender.powerLevel.role !=
PowerLevelRole.user &&
!nextEventSameSender &&
!ownMessage &&
!event.room.isDirectChat)
Padding(
padding: const EdgeInsets.only(
right: 2.0,
if (!card)
Padding(
padding: EdgeInsets.only(left: 8.0),
child: Row(
mainAxisAlignment: card
? .start
: ownMessage
? .end
: .start,
children: [
if (sender.powerLevel.role !=
PowerLevelRole.user &&
!nextEventSameSender &&
!ownMessage &&
!event.room.isDirectChat)
Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(
sender.powerLevel.role ==
PowerLevelRole.moderator
? Icons.add_moderator_outlined
: Icons.admin_panel_settings,
size: 14,
color: theme
.colorScheme
.onPrimaryContainer,
),
),
child: Icon(
sender.powerLevel.role ==
PowerLevelRole.moderator
? Icons.add_moderator_outlined
: Icons.admin_panel_settings,
size: 14,
color: theme
.colorScheme
.onPrimaryContainer,
),
),
if ((!nextEventSameSender) && !ownMessage)
FutureBuilder<User?>(
future: event.fetchSenderUser(),
builder: (context, snapshot) {
final displayname =
snapshot.data?.calcDisplayname() ??
sender.calcDisplayname();
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 200,
),
child: Text(
displayname,
style: TextStyle(
fontWeight: FontWeight.bold,
color: event.room.isDirectChat
? Colors.transparent
: (theme.brightness ==
Brightness.light
? displayname
.colorScheme
.primary
: displayname
.colorScheme
.primaryContainer),
fontSize: 11,
shadows: event.room.isDirectChat
? null
: wallpaperTextShadow,
if ((!nextEventSameSender) && !ownMessage)
FutureBuilder<User?>(
future: event.fetchSenderUser(),
builder: (context, snapshot) {
final displayname =
snapshot.data
?.calcDisplayname() ??
sender.calcDisplayname();
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 200,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
),
],
child: Text(
displayname,
style: TextStyle(
fontWeight: FontWeight.bold,
color: event.room.isDirectChat
? Colors.transparent
: (theme.brightness ==
Brightness.light
? displayname
.colorScheme
.primary
: displayname
.colorScheme
.primaryContainer),
fontSize: 11,
shadows: event.room.isDirectChat
? null
: wallpaperTextShadow,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
),
],
),
),
),
Container(
alignment: alignment,
@@ -438,6 +472,7 @@ class Message extends StatelessWidget {
),
animateIn: animateIn,
child: Container(
width: card ? double.infinity : null,
decoration: BoxDecoration(
color: noBubble
? Colors.transparent
@@ -458,15 +493,108 @@ class Message extends StatelessWidget {
AppConfig.borderRadius,
),
),
constraints: const BoxConstraints(
maxWidth:
FluffyThemes.columnWidth * 1.5,
constraints: BoxConstraints(
maxWidth: card
? double.infinity
: FluffyThemes.columnWidth * 1.5,
),
child: Column(
mainAxisSize: .min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
if (card && showSenderName)
Container(
padding:
const EdgeInsets.fromLTRB(
16,
12,
16,
0,
),
child: Row(
children: [
if (sender
.powerLevel
.role !=
PowerLevelRole.user)
Padding(
padding:
const EdgeInsets.only(
right: 2.0,
),
child: Icon(
sender
.powerLevel
.role ==
PowerLevelRole
.moderator
? Icons
.add_moderator_outlined
: Icons
.admin_panel_settings,
size: 14,
color: theme
.colorScheme
.onPrimaryContainer,
),
),
Expanded(
child: FutureBuilder<User?>(
future: event
.fetchSenderUser(),
builder: (context, snapshot) {
final displayname =
snapshot.data
?.calcDisplayname() ??
sender
.calcDisplayname();
return Text(
displayname,
style: TextStyle(
fontWeight:
FontWeight
.bold,
color:
theme.brightness ==
Brightness
.light
? displayname
.colorScheme
.primary
: displayname
.colorScheme
.primaryContainer,
fontSize: 12,
),
maxLines: 1,
overflow:
TextOverflow
.ellipsis,
);
},
),
),
if (displayTime &&
event
.status
.isSent) ...[
const SizedBox(width: 8),
Text(
event.originServerTs
.localizedTimeOfDay(
context,
),
style: TextStyle(
color:
eventStateTextColor,
fontSize: 11,
),
),
],
],
),
),
if (event.inReplyToEventId(
includingFallback: false,
) !=
@@ -557,7 +685,9 @@ class Message extends StatelessWidget {
child: !hasReactions
? const SizedBox.shrink()
: Container(
alignment: ownMessage
alignment: card
? Alignment.centerLeft
: ownMessage
? Alignment.centerRight
: Alignment.centerLeft,
padding: EdgeInsets.only(
@@ -569,7 +699,11 @@ class Message extends StatelessWidget {
),
),
Row(
mainAxisAlignment: ownMessage ? .end : .start,
mainAxisAlignment: card
? .start
: ownMessage
? .end
: .start,
children: [
const SizedBox(width: 8),
if (event.status.isSent &&
@@ -643,7 +777,9 @@ class Message extends StatelessWidget {
],
),
Align(
alignment: ownMessage
alignment: card
? Alignment.bottomLeft
: ownMessage
? Alignment.bottomRight
: Alignment.bottomLeft,
child: AnimatedSize(
@@ -861,19 +997,24 @@ class Message extends StatelessWidget {
child: threadChildren.isEmpty
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.only(
top: 2.0,
bottom: 8.0,
left: avatarSize + 8,
),
padding: const EdgeInsets.only(top: 2.0, bottom: 8.0)
.add(
EdgeInsets.only(
left: card ? 8.0 : avatarSize + 8,
),
),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
constraints: BoxConstraints(
maxWidth: card
? double.infinity
: FluffyThemes.columnWidth * 1.5,
),
child: TextButton.icon(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
foregroundColor:
theme.colorScheme.onSecondaryContainer,