✨ Rendering stickers inside content
This commit is contained in:
parent
e61497dc70
commit
8efd8cd58e
@ -380,13 +380,16 @@ class _LoginLookupScreen extends HookConsumerWidget {
|
||||
data: {
|
||||
'account': uname,
|
||||
'device_id': await FlutterUdid.consistentUdid,
|
||||
'platform': switch (defaultTargetPlatform) {
|
||||
'platform':
|
||||
kIsWeb
|
||||
? 1
|
||||
: switch (defaultTargetPlatform) {
|
||||
TargetPlatform.iOS => 2,
|
||||
TargetPlatform.android => 3,
|
||||
TargetPlatform.macOS => 4,
|
||||
TargetPlatform.windows => 5,
|
||||
TargetPlatform.linux => 6,
|
||||
_ => 1,
|
||||
_ => 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -12,11 +12,9 @@ import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/database.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/services/rtc.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
@ -473,21 +471,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
error: (_, __) => const Text('Error'),
|
||||
),
|
||||
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(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
|
@ -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();
|
||||
}
|
@ -7,6 +7,8 @@ import 'package:flutter_platform_alert/flutter_platform_alert.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
// TODO support web here
|
||||
|
||||
String _parseRemoteError(DioException err) {
|
||||
log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
|
||||
if (err.response?.data is String) return err.response?.data;
|
||||
|
@ -6,21 +6,48 @@ class UniversalImage extends StatelessWidget {
|
||||
final String uri;
|
||||
final String? blurHash;
|
||||
final BoxFit fit;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final bool noCacheOptimization;
|
||||
|
||||
const UniversalImage({
|
||||
super.key,
|
||||
required this.uri,
|
||||
this.blurHash,
|
||||
this.fit = BoxFit.cover,
|
||||
this.width,
|
||||
this.height,
|
||||
this.noCacheOptimization = false,
|
||||
});
|
||||
|
||||
@override
|
||||
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,
|
||||
children: [
|
||||
if (blurHash != null) BlurHash(hash: blurHash!),
|
||||
CachedNetworkImage(imageUrl: uri, fit: fit),
|
||||
CachedNetworkImage(
|
||||
imageUrl: uri,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
memCacheHeight: cacheHeight,
|
||||
memCacheWidth: cacheWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,16 @@ class UniversalImage extends StatelessWidget {
|
||||
final String uri;
|
||||
final String? blurHash;
|
||||
final BoxFit fit;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const UniversalImage({
|
||||
super.key,
|
||||
required this.uri,
|
||||
this.blurHash,
|
||||
this.fit = BoxFit.cover,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -19,8 +24,8 @@ class UniversalImage extends StatelessWidget {
|
||||
onElementCreated: (element) {
|
||||
element as web.HTMLImageElement;
|
||||
element.src = uri;
|
||||
element.style.width = '100%';
|
||||
element.style.height = '100%';
|
||||
element.style.width = width?.toString() ?? '100%';
|
||||
element.style.height = height?.toString() ?? '100%';
|
||||
element.style.objectFit = switch (fit) {
|
||||
BoxFit.cover || BoxFit.fitWidth || BoxFit.fitHeight => 'cover',
|
||||
BoxFit.fill => 'fill',
|
||||
|
@ -1,16 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlight/flutter_highlight.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_latex/flutter_markdown_latex.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:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class MarkdownTextContent extends StatelessWidget {
|
||||
import 'image.dart';
|
||||
|
||||
class MarkdownTextContent extends HookConsumerWidget {
|
||||
final String content;
|
||||
final bool isAutoWarp;
|
||||
final bool isEnlargeSticker;
|
||||
final TextScaler? textScaler;
|
||||
final Color? textColor;
|
||||
|
||||
@ -18,13 +22,24 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
super.key,
|
||||
required this.content,
|
||||
this.isAutoWarp = false,
|
||||
this.isEnlargeSticker = false,
|
||||
this.textScaler,
|
||||
this.textColor,
|
||||
});
|
||||
|
||||
@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(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@ -70,6 +85,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
|
||||
if (isAutoWarp) markdown.LineBreakSyntax(),
|
||||
_UserNameCardInlineSyntax(),
|
||||
_StickerInlineSyntax(ref.read(serverUrlProvider)),
|
||||
markdown.AutolinkSyntax(),
|
||||
markdown.AutolinkExtensionSyntax(),
|
||||
markdown.CodeSyntax(),
|
||||
@ -80,6 +96,35 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
if (href == null) return;
|
||||
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 {
|
||||
@override
|
||||
Widget? visitElementAfterWithContext(
|
||||
|
@ -23,7 +23,6 @@ import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import sqlite3_flutter_libs
|
||||
import super_native_extensions
|
||||
import tencent_rtc_sdk
|
||||
import url_launcher_macos
|
||||
import volume_controller
|
||||
import wakelock_plus
|
||||
@ -47,7 +46,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
|
||||
TencentRTCCloud.register(with: registry.registrar(forPlugin: "TencentRTCCloud"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
|
@ -1770,14 +1770,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -91,7 +91,6 @@ dependencies:
|
||||
flutter_expandable_fab: ^2.5.0
|
||||
markdown_editor_plus: ^0.2.15
|
||||
croppy: ^1.3.6
|
||||
tencent_rtc_sdk: ^12.3.6
|
||||
table_calendar: ^3.1.3
|
||||
relative_time: ^5.0.0
|
||||
dropdown_button2: ^2.3.9
|
||||
|
@ -17,7 +17,6 @@
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.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 <volume_controller/volume_controller_plugin_c_api.h>
|
||||
|
||||
@ -44,8 +43,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi"));
|
||||
TrtcPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("TrtcPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
VolumeControllerPluginCApiRegisterWithRegistrar(
|
||||
|
@ -14,7 +14,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
media_kit_video
|
||||
sqlite3_flutter_libs
|
||||
super_native_extensions
|
||||
tencent_rtc_sdk
|
||||
url_launcher_windows
|
||||
volume_controller
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user