Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cspell/nextcloud.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apirequest
apppassword
bigfilechunking
boardname
bools
bulkupload
clearsky
Expand Down Expand Up @@ -44,6 +45,7 @@ shareapi
sharebymail
sharee
shareesapi
stackname
statuscode
stime
stunservers
Expand Down
114 changes: 113 additions & 1 deletion packages/neon/neon_talk/lib/src/widgets/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import 'package:neon_framework/theme.dart';
import 'package:neon_talk/l10n/localizations.dart';
import 'package:neon_talk/src/widgets/actor_avatar.dart';
import 'package:neon_talk/src/widgets/reactions.dart';
import 'package:neon_talk/src/widgets/rich_object/deck_card.dart';
import 'package:neon_talk/src/widgets/rich_object/fallback.dart';
import 'package:neon_talk/src/widgets/rich_object/file.dart';
import 'package:neon_talk/src/widgets/rich_object/mention.dart';
import 'package:nextcloud/spreed.dart' as spreed;
import 'package:nextcloud/utils.dart';
import 'package:timezone/timezone.dart' as tz;
Expand Down Expand Up @@ -35,9 +39,117 @@ TextSpan buildChatMessage({
message = message.replaceAll('\n', ' ');
}

final unusedParameters = <String, spreed.RichObjectParameter>{};

var parts = [message];
for (final entry in chatMessage.messageParameters.entries) {
final newParts = <String>[];

var found = false;
for (final part in parts) {
final p = part.split('{${entry.key}}');
newParts.addAll(p.intersperse('{${entry.key}}'));
if (p.length > 1) {
found = true;
}
}

if (!found) {
unusedParameters[entry.key] = entry.value;
}

parts = newParts;
}

final children = <InlineSpan>[];

for (final entry in unusedParameters.entries) {
if (entry.key == 'actor' || entry.key == 'user') {
continue;
}

children
..add(
buildRichObjectParameter(
parameter: entry.value,
textStyle: style,
isPreview: isPreview,
),
)
..add(const TextSpan(text: '\n'));
}

for (final part in parts) {
var match = false;
for (final entry in chatMessage.messageParameters.entries) {
if ('{${entry.key}}' == part) {
children.add(
buildRichObjectParameter(
parameter: entry.value,
textStyle: style,
isPreview: isPreview,
),
);
match = true;
break;
}
}

if (!match) {
children.add(
TextSpan(
text: part,
),
);
}
}

return TextSpan(
text: message,
style: style,
children: children,
);
}

/// Renders a rich object [parameter] to be interactive.
InlineSpan buildRichObjectParameter({
required spreed.RichObjectParameter parameter,
required TextStyle? textStyle,
required bool isPreview,
}) {
Widget child;

const mentionTypes = ['user', 'call', 'guest', 'user-group', 'group'];
if (mentionTypes.contains(parameter.type)) {
child = TalkRichObjectMention(
parameter: parameter,
textStyle: textStyle,
);
} else {
if (isPreview) {
child = Text(parameter.name);
} else {
switch (parameter.type) {
case 'file':
child = TalkRichObjectFile(
parameter: parameter,
textStyle: textStyle,
);
case 'deck-card':
child = TalkRichObjectDeckCard(
parameter: parameter,
);
default:
child = TalkRichObjectFallback(
parameter: parameter,
textStyle: textStyle,
);
}
}
}

return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: child,
);
}

Expand Down
36 changes: 36 additions & 0 deletions packages/neon/neon_talk/lib/src/widgets/rich_object/deck_card.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:nextcloud/spreed.dart' as spreed;

/// Widget to display a Deck card from a rich object.
class TalkRichObjectDeckCard extends StatelessWidget {
/// Creates a new Talk rich object Deck card.
const TalkRichObjectDeckCard({
required this.parameter,
super.key,
});

/// The parameter to display.
final spreed.RichObjectParameter parameter;

@override
Widget build(BuildContext context) {
return IntrinsicWidth(
child: Card(
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () {
context.go(parameter.link!);
},
child: ListTile(
// TODO: Use the actual Deck logo
leading: const Icon(MdiIcons.cardMultiple),
title: Text(parameter.name),
subtitle: Text('${parameter.boardname!}: ${parameter.stackname!}'),
),
),
),
);
}
}
58 changes: 58 additions & 0 deletions packages/neon/neon_talk/lib/src/widgets/rich_object/fallback.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:neon_framework/widgets.dart';
import 'package:nextcloud/spreed.dart' as spreed;

/// Widget used to render rich object parameters with unknown types.
class TalkRichObjectFallback extends StatelessWidget {
/// Creates a new Talk rich object fallback
const TalkRichObjectFallback({
required this.parameter,
required this.textStyle,
super.key,
});

/// The parameter to display.
final spreed.RichObjectParameter parameter;

/// The TextStyle to applied to all text elements in this rich object.
final TextStyle? textStyle;

@override
Widget build(BuildContext context) {
final iconUrl = parameter.iconUrl;

EdgeInsetsGeometry? labelPadding;
Widget? avatar;
if (iconUrl != null) {
labelPadding = const EdgeInsetsDirectional.only(end: 8);
avatar = Padding(
padding: const EdgeInsets.all(4),
child: CircleAvatar(
child: ClipOval(
child: NeonUriImage(
uri: Uri.parse(iconUrl),
),
),
),
);
}

return ActionChip(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
padding: EdgeInsets.zero,
labelPadding: labelPadding,
avatar: avatar,
label: Text(
parameter.name,
style: textStyle,
),
onPressed: () {
final link = parameter.link;
if (link != null) {
context.go(link);
}
},
);
}
}
98 changes: 98 additions & 0 deletions packages/neon/neon_talk/lib/src/widgets/rich_object/file.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'package:file_icons/file_icons.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/theme.dart';
import 'package:neon_framework/utils.dart';
import 'package:neon_framework/widgets.dart';
import 'package:nextcloud/core.dart';
import 'package:nextcloud/spreed.dart' as spreed;

/// Displays a file preview from a rich object.
class TalkRichObjectFile extends StatelessWidget {
/// Creates a new Talk rich object file.
const TalkRichObjectFile({
required this.parameter,
required this.textStyle,
super.key,
});

/// The parameter to display.
final spreed.RichObjectParameter parameter;

/// The TextStyle to applied to all text elements in this rich object.
final TextStyle? textStyle;

@override
Widget build(BuildContext context) {
Widget child;

if (parameter.previewAvailable == spreed.RichObjectParameter_PreviewAvailable.yes) {
final maxHeight = MediaQuery.sizeOf(context).height / 2;

var width = parameter.width;
var height = parameter.height;
if (width != null && height != null) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;

var size = Size(width.toDouble(), height.toDouble());

// Convert to logical pixels
size /= devicePixelRatio;

// Constrain size to max height but keep aspect ration
if (size.height > maxHeight) {
size = size * (maxHeight / size.height);
}

// Convert back to device pixels
size *= devicePixelRatio;

width = size.width.toInt();
height = size.height.toInt();
}

child = ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxHeight,
),
child: Tooltip(
message: parameter.name,
child: NeonApiImage(
cacheKey: 'preview-${parameter.path!}-$width-$height',
etag: parameter.etag,
expires: null,
request: (client) => client.core.preview.$getPreviewByFileId_Request(
fileId: int.parse(parameter.id),
x: width,
y: height,
),
),
),
);
} else {
child = Row(
children: [
FileIcon(
parameter.name,
color: Theme.of(context).colorScheme.primary,
size: largeIconSize,
),
Text(
parameter.name,
style: textStyle,
),
],
);
}

return InkWell(
onTap: () {
final link = Uri.parse(parameter.link!);
final account = NeonProvider.of<AccountsBloc>(context).activeAccount.value!;
context.go(account.completeUri(link).toString());
},
child: child,
);
}
}
Loading