Rendering stickers inside content

This commit is contained in:
LittleSheep 2025-05-11 23:28:04 +08:00
parent e61497dc70
commit 8efd8cd58e
12 changed files with 117 additions and 103 deletions

View File

@ -380,13 +380,16 @@ class _LoginLookupScreen extends HookConsumerWidget {
data: { data: {
'account': uname, 'account': uname,
'device_id': await FlutterUdid.consistentUdid, 'device_id': await FlutterUdid.consistentUdid,
'platform': switch (defaultTargetPlatform) { 'platform':
kIsWeb
? 1
: switch (defaultTargetPlatform) {
TargetPlatform.iOS => 2, TargetPlatform.iOS => 2,
TargetPlatform.android => 3, TargetPlatform.android => 3,
TargetPlatform.macOS => 4, TargetPlatform.macOS => 4,
TargetPlatform.windows => 5, TargetPlatform.windows => 5,
TargetPlatform.linux => 6, TargetPlatform.linux => 6,
_ => 1, _ => 0,
}, },
}, },
); );

View File

@ -12,11 +12,9 @@ import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/database.dart'; import 'package:island/pods/database.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/screens/posts/compose.dart'; import 'package:island/screens/posts/compose.dart';
import 'package:island/services/rtc.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart'; import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@ -473,21 +471,6 @@ class ChatRoomScreen extends HookConsumerWidget {
error: (_, __) => const Text('Error'), error: (_, __) => const Text('Error'),
), ),
actions: [ actions: [
IconButton(
onPressed: () {
final user = ref.watch(userInfoProvider);
if (currentlyJoined) {
leaveRealtimeChat(chatRoom.value!);
} else {
joinRealtimeChat(
ref.watch(apiClientProvider),
chatRoom.value!,
user.value!,
);
}
},
icon: const Icon(Symbols.video_call),
),
IconButton( IconButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
onPressed: () { onPressed: () {

View File

@ -1,51 +0,0 @@
import 'package:dio/dio.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/user.dart';
import 'package:tencent_rtc_sdk/trtc_cloud.dart';
import 'package:tencent_rtc_sdk/trtc_cloud_def.dart';
import 'package:tencent_rtc_sdk/trtc_cloud_listener.dart';
Future<TRTCCloud> _getTRTCCloud() async {
return TRTCCloud.sharedInstance();
}
bool currentlyJoined = false;
Future<void> joinRealtimeChat(
Dio apiClient,
SnChatRoom chat,
SnAccount user,
) async {
final resp = await apiClient.get('/chat/realtime/${chat.id}');
final data = ChatRealtimeJoinResponse.fromJson(resp.data);
final config = data.config;
final cloud = await _getTRTCCloud();
cloud.setConsoleEnabled(true);
cloud.registerListener(
TRTCCloudListener(
onRemoteUserEnterRoom: (userId) {
print('onRemoteUserEnterRoom: $userId');
},
onRemoteUserLeaveRoom: (userId, reason) {
print('onRemoteUserLeaveRoom: $userId, $reason');
},
),
);
cloud.enterRoom(
TRTCParams(
sdkAppId: config['app_id'],
userId: user.name,
userSig: data.token,
roomId: chat.id,
role: TRTCRoleType.anchor,
),
TRTCAppScene.voiceChatRoom,
);
cloud.startLocalAudio(TRTCAudioQuality.speech);
currentlyJoined = true;
}
Future<void> leaveRealtimeChat(SnChatRoom chat) async {
final cloud = await _getTRTCCloud();
cloud.exitRoom();
}

View File

@ -7,6 +7,8 @@ import 'package:flutter_platform_alert/flutter_platform_alert.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
// TODO support web here
String _parseRemoteError(DioException err) { String _parseRemoteError(DioException err) {
log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}'); log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
if (err.response?.data is String) return err.response?.data; if (err.response?.data is String) return err.response?.data;

View File

@ -6,21 +6,48 @@ class UniversalImage extends StatelessWidget {
final String uri; final String uri;
final String? blurHash; final String? blurHash;
final BoxFit fit; final BoxFit fit;
final double? width;
final double? height;
final bool noCacheOptimization;
const UniversalImage({ const UniversalImage({
super.key, super.key,
required this.uri, required this.uri,
this.blurHash, this.blurHash,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.width,
this.height,
this.noCacheOptimization = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( int? cacheWidth;
int? cacheHeight;
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;
}
return SizedBox(
width: width,
height: height,
child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (blurHash != null) BlurHash(hash: blurHash!), if (blurHash != null) BlurHash(hash: blurHash!),
CachedNetworkImage(imageUrl: uri, fit: fit), CachedNetworkImage(
imageUrl: uri,
fit: fit,
width: width,
height: height,
memCacheHeight: cacheHeight,
memCacheWidth: cacheWidth,
),
], ],
),
); );
} }
} }

View File

@ -5,11 +5,16 @@ class UniversalImage extends StatelessWidget {
final String uri; final String uri;
final String? blurHash; final String? blurHash;
final BoxFit fit; final BoxFit fit;
final double? width;
final double? height;
const UniversalImage({ const UniversalImage({
super.key, super.key,
required this.uri, required this.uri,
this.blurHash, this.blurHash,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.width,
this.height,
}); });
@override @override
@ -19,8 +24,8 @@ class UniversalImage extends StatelessWidget {
onElementCreated: (element) { onElementCreated: (element) {
element as web.HTMLImageElement; element as web.HTMLImageElement;
element.src = uri; element.src = uri;
element.style.width = '100%'; element.style.width = width?.toString() ?? '100%';
element.style.height = '100%'; element.style.height = height?.toString() ?? '100%';
element.style.objectFit = switch (fit) { element.style.objectFit = switch (fit) {
BoxFit.cover || BoxFit.fitWidth || BoxFit.fitHeight => 'cover', BoxFit.cover || BoxFit.fitWidth || BoxFit.fitHeight => 'cover',
BoxFit.fill => 'fill', BoxFit.fill => 'fill',

View File

@ -1,16 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_highlight/flutter_highlight.dart'; import 'package:flutter_highlight/flutter_highlight.dart';
import 'package:flutter_highlight/theme_map.dart'; import 'package:flutter_highlight/theme_map.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_markdown_latex/flutter_markdown_latex.dart'; import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class MarkdownTextContent extends StatelessWidget { import 'image.dart';
class MarkdownTextContent extends HookConsumerWidget {
final String content; final String content;
final bool isAutoWarp; final bool isAutoWarp;
final bool isEnlargeSticker;
final TextScaler? textScaler; final TextScaler? textScaler;
final Color? textColor; final Color? textColor;
@ -18,13 +22,24 @@ class MarkdownTextContent extends StatelessWidget {
super.key, super.key,
required this.content, required this.content,
this.isAutoWarp = false, this.isAutoWarp = false,
this.isEnlargeSticker = false,
this.textScaler, this.textScaler,
this.textColor, this.textColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final baseUrl = ref.watch(serverUrlProvider);
final doesEnlargeSticker = useMemoized(() {
// Check if content only contains one sticker by matching the sticker pattern
final stickerPattern = RegExp(r':([-\w]+):');
final matches = stickerPattern.allMatches(content);
// Content should only contain one sticker and nothing else (except whitespace)
final contentWithoutStickers =
content.replaceAll(stickerPattern, '').trim();
return matches.length == 1 && contentWithoutStickers.isEmpty;
}, [content]);
return Markdown( return Markdown(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -70,6 +85,7 @@ class MarkdownTextContent extends StatelessWidget {
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes, ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
if (isAutoWarp) markdown.LineBreakSyntax(), if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(), _UserNameCardInlineSyntax(),
_StickerInlineSyntax(ref.read(serverUrlProvider)),
markdown.AutolinkSyntax(), markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(), markdown.CodeSyntax(),
@ -80,6 +96,35 @@ class MarkdownTextContent extends StatelessWidget {
if (href == null) return; if (href == null) return;
await launchUrlString(href, mode: LaunchMode.externalApplication); await launchUrlString(href, mode: LaunchMode.externalApplication);
}, },
imageBuilder: (uri, title, alt) {
if (uri.scheme == 'solink') {
switch (uri.host) {
case 'stickers':
final size = doesEnlargeSticker ? 96.0 : 24.0;
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: UniversalImage(
uri: '$baseUrl/stickers/lookup/${uri.pathSegments[0]}/open',
width: size,
height: size,
fit: BoxFit.cover,
noCacheOptimization: true,
),
),
);
}
}
final content = UniversalImage(uri: uri.toString(), fit: BoxFit.cover);
if (alt != null) {
return Tooltip(message: alt, child: content);
}
return content;
},
); );
} }
} }
@ -100,6 +145,21 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
} }
} }
class _StickerInlineSyntax extends markdown.InlineSyntax {
final String baseUrl;
_StickerInlineSyntax(this.baseUrl) : super(r':([-\w]+):');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final placeholder = match[1]!;
final image = markdown.Element.text('img', '')
..attributes['src'] = Uri.encodeFull('solink://stickers/$placeholder');
parser.addNode(image);
return true;
}
}
class HighlightBuilder extends MarkdownElementBuilder { class HighlightBuilder extends MarkdownElementBuilder {
@override @override
Widget? visitElementAfterWithContext( Widget? visitElementAfterWithContext(

View File

@ -23,7 +23,6 @@ import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import sqlite3_flutter_libs import sqlite3_flutter_libs
import super_native_extensions import super_native_extensions
import tencent_rtc_sdk
import url_launcher_macos import url_launcher_macos
import volume_controller import volume_controller
import wakelock_plus import wakelock_plus
@ -47,7 +46,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
TencentRTCCloud.register(with: registry.registrar(forPlugin: "TencentRTCCloud"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))

View File

@ -1770,14 +1770,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
tencent_rtc_sdk:
dependency: "direct main"
description:
name: tencent_rtc_sdk
sha256: "0ec7f3a32c443573a0509d06bf390e11a709fca338a46645c1d4dbc3f17f029a"
url: "https://pub.dev"
source: hosted
version: "12.3.6"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:

View File

@ -91,7 +91,6 @@ dependencies:
flutter_expandable_fab: ^2.5.0 flutter_expandable_fab: ^2.5.0
markdown_editor_plus: ^0.2.15 markdown_editor_plus: ^0.2.15
croppy: ^1.3.6 croppy: ^1.3.6
tencent_rtc_sdk: ^12.3.6
table_calendar: ^3.1.3 table_calendar: ^3.1.3
relative_time: ^5.0.0 relative_time: ^5.0.0
dropdown_button2: ^2.3.9 dropdown_button2: ^2.3.9

View File

@ -17,7 +17,6 @@
#include <media_kit_video/media_kit_video_plugin_c_api.h> #include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin_c_api.h> #include <super_native_extensions/super_native_extensions_plugin_c_api.h>
#include <tencent_rtc_sdk/trtc_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <volume_controller/volume_controller_plugin_c_api.h> #include <volume_controller/volume_controller_plugin_c_api.h>
@ -44,8 +43,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
SuperNativeExtensionsPluginCApiRegisterWithRegistrar( SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi"));
TrtcPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TrtcPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VolumeControllerPluginCApiRegisterWithRegistrar( VolumeControllerPluginCApiRegisterWithRegistrar(

View File

@ -14,7 +14,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_video media_kit_video
sqlite3_flutter_libs sqlite3_flutter_libs
super_native_extensions super_native_extensions
tencent_rtc_sdk
url_launcher_windows url_launcher_windows
volume_controller volume_controller
) )