✨ Rendering stickers inside content
This commit is contained in:
		@@ -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
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user