Add basic rendering of formatted text
This commit is contained in:
parent
0c5e354a31
commit
cc81d375e0
12 changed files with 338 additions and 26 deletions
|
@ -26,6 +26,6 @@ subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:developer';
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:contacts_plus_plus/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/apis/message_api.dart';
|
import 'package:contacts_plus_plus/apis/message_api.dart';
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||||
|
import 'package:contacts_plus_plus/string_formatter.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
enum MessageType {
|
enum MessageType {
|
||||||
|
@ -43,11 +44,13 @@ class Message implements Comparable {
|
||||||
final String senderId;
|
final String senderId;
|
||||||
final MessageType type;
|
final MessageType type;
|
||||||
final String content;
|
final String content;
|
||||||
|
final FormatNode formattedContent;
|
||||||
final DateTime sendTime;
|
final DateTime sendTime;
|
||||||
final MessageState state;
|
final MessageState state;
|
||||||
|
|
||||||
Message({required this.id, required this.recipientId, required this.senderId, required this.type,
|
Message({required this.id, required this.recipientId, required this.senderId, required this.type,
|
||||||
required this.content, required this.sendTime, this.state=MessageState.local});
|
required this.content, required this.sendTime, this.state=MessageState.local})
|
||||||
|
: formattedContent = FormatNode.fromText(content);
|
||||||
|
|
||||||
factory Message.fromMap(Map map, {MessageState? withState}) {
|
factory Message.fromMap(Map map, {MessageState? withState}) {
|
||||||
final typeString = (map["messageType"] as String?) ?? "";
|
final typeString = (map["messageType"] as String?) ?? "";
|
||||||
|
@ -68,10 +71,21 @@ class Message implements Comparable {
|
||||||
|
|
||||||
Message copy() => copyWith();
|
Message copy() => copyWith();
|
||||||
|
|
||||||
Message copyWith({String? id, String? recipientId, String? senderId, MessageType? type, String? content,
|
Message copyWith({
|
||||||
DateTime? sendTime, MessageState? state}) {
|
String? id,
|
||||||
return Message(id: id ?? this.id, recipientId: recipientId ?? this.recipientId, senderId: senderId ?? this.senderId,
|
String? recipientId,
|
||||||
type: type ?? this.type, content: content ?? this.content, sendTime: sendTime ?? this.sendTime,
|
String? senderId,
|
||||||
|
MessageType? type,
|
||||||
|
String? content,
|
||||||
|
DateTime? sendTime,
|
||||||
|
MessageState? state}) {
|
||||||
|
return Message(
|
||||||
|
id: id ?? this.id,
|
||||||
|
recipientId: recipientId ?? this.recipientId,
|
||||||
|
senderId: senderId ?? this.senderId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
content: content ?? this.content,
|
||||||
|
sendTime: sendTime ?? this.sendTime,
|
||||||
state: state ?? this.state
|
state: state ?? this.state
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
import 'package:contacts_plus_plus/string_formatter.dart';
|
||||||
|
|
||||||
class Session {
|
class Session {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
|
final FormatNode formattedName;
|
||||||
final List<SessionUser> sessionUsers;
|
final List<SessionUser> sessionUsers;
|
||||||
final String thumbnail;
|
final String thumbnail;
|
||||||
final int maxUsers;
|
final int maxUsers;
|
||||||
final bool hasEnded;
|
final bool hasEnded;
|
||||||
final bool isValid;
|
final bool isValid;
|
||||||
final String description;
|
final String description;
|
||||||
|
final FormatNode formattedDescription;
|
||||||
final List<String> tags;
|
final List<String> tags;
|
||||||
final bool headlessHost;
|
final bool headlessHost;
|
||||||
final String hostUsername;
|
final String hostUsername;
|
||||||
|
@ -15,7 +19,7 @@ class Session {
|
||||||
Session({required this.id, required this.name, required this.sessionUsers, required this.thumbnail,
|
Session({required this.id, required this.name, required this.sessionUsers, required this.thumbnail,
|
||||||
required this.maxUsers, required this.hasEnded, required this.isValid, required this.description,
|
required this.maxUsers, required this.hasEnded, required this.isValid, required this.description,
|
||||||
required this.tags, required this.headlessHost, required this.hostUsername, required this.accessLevel,
|
required this.tags, required this.headlessHost, required this.hostUsername, required this.accessLevel,
|
||||||
});
|
}) : formattedName = FormatNode.fromText(name), formattedDescription = FormatNode.fromText(description);
|
||||||
|
|
||||||
factory Session.fromMap(Map map) {
|
factory Session.fromMap(Map map) {
|
||||||
return Session(
|
return Session(
|
||||||
|
|
221
lib/string_formatter.dart
Normal file
221
lib/string_formatter.dart
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import 'package:color/color.dart' as cc;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FormatNode {
|
||||||
|
String text;
|
||||||
|
final FormatData format;
|
||||||
|
final List<FormatNode> children;
|
||||||
|
|
||||||
|
FormatNode({required this.text, required this.format, required this.children});
|
||||||
|
|
||||||
|
bool get isUnformatted => format.isUnformatted && children.isEmpty;
|
||||||
|
|
||||||
|
bool get isEmpty => text.isEmpty && children.isEmpty;
|
||||||
|
|
||||||
|
factory FormatNode.unformatted(String? text) {
|
||||||
|
return FormatNode(text: text ?? "", format: FormatData.unformatted(), children: const []);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory FormatNode.fromText(String? text) {
|
||||||
|
if (text == null) return FormatNode.unformatted(text);
|
||||||
|
var tags = FormatTag.parseTags(text);
|
||||||
|
if (tags.isEmpty) return FormatNode.unformatted(text);
|
||||||
|
final root = FormatNode(
|
||||||
|
format: FormatData.unformatted(),
|
||||||
|
text: text.substring(0, tags.first.startIndex),
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final activeTags = <FormatData>[];
|
||||||
|
|
||||||
|
for (int i = 0; i < tags.length; i++) {
|
||||||
|
final tag = tags[i];
|
||||||
|
final substr = text.substring(tag.endIndex, (i + 1 < tags.length) ? tags[i + 1].startIndex : null);
|
||||||
|
if (tag.format.isAdditive) {
|
||||||
|
activeTags.add(tag.format);
|
||||||
|
} else {
|
||||||
|
final idx = activeTags.lastIndexWhere((element) => element.name == tag.format.name);
|
||||||
|
if (idx != -1) {
|
||||||
|
activeTags.removeAt(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (substr.isNotEmpty) {
|
||||||
|
root.children.add(
|
||||||
|
FormatNode.buildFromStyles(activeTags, substr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSpan toTextSpan({required TextStyle baseStyle}) {
|
||||||
|
final spanTree = TextSpan(
|
||||||
|
text: text,
|
||||||
|
style: format.isUnformatted ? baseStyle : format.style(),
|
||||||
|
children: children.map((e) => e.toTextSpan(baseStyle: baseStyle)).toList()
|
||||||
|
);
|
||||||
|
return spanTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FormatNode buildFromStyles(List<FormatData> styles, String text) {
|
||||||
|
if (styles.isEmpty) return FormatNode(format: FormatData.unformatted(), children: [], text: text);
|
||||||
|
final root = FormatNode(text: "", format: styles.first, children: []);
|
||||||
|
var current = root;
|
||||||
|
for (final style in styles.sublist(1)) {
|
||||||
|
final next = FormatNode(text: "", format: style, children: []);
|
||||||
|
current.children.add(next);
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
current.text = text;
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FormatTag {
|
||||||
|
final int startIndex;
|
||||||
|
final int endIndex;
|
||||||
|
final FormatData format;
|
||||||
|
|
||||||
|
const FormatTag({
|
||||||
|
required this.startIndex,
|
||||||
|
required this.endIndex,
|
||||||
|
required this.format,
|
||||||
|
});
|
||||||
|
|
||||||
|
static List<FormatTag> parseTags(String text) {
|
||||||
|
final startMatches = RegExp(r"<(.+?)>").allMatches(text);
|
||||||
|
|
||||||
|
final spans = <FormatTag>[];
|
||||||
|
|
||||||
|
for (final startMatch in startMatches) {
|
||||||
|
final fullTag = startMatch.group(1);
|
||||||
|
if (fullTag == null) continue;
|
||||||
|
final tag = FormatData.parse(fullTag);
|
||||||
|
spans.add(
|
||||||
|
FormatTag(
|
||||||
|
startIndex: startMatch.start,
|
||||||
|
endIndex: startMatch.end,
|
||||||
|
format: tag,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FormatAction {
|
||||||
|
final String Function(String input, String parameter)? transform;
|
||||||
|
final TextStyle Function(String? parameter, TextStyle baseStyle)? style;
|
||||||
|
|
||||||
|
FormatAction({this.transform, this.style});
|
||||||
|
}
|
||||||
|
|
||||||
|
class FormatData {
|
||||||
|
static Color? tryParseColor(String? text) {
|
||||||
|
if (text == null) return null;
|
||||||
|
var color = cc.RgbColor.namedColors[text];
|
||||||
|
if (color != null) {
|
||||||
|
return Color.fromARGB(255, color.r.round(), color.g.round(), color.b.round());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
color = cc.HexColor(text);
|
||||||
|
return Color.fromARGB(255, color.r.round(), color.g.round(), color.b.round());
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final Map<String, FormatAction> _richTextTags = {
|
||||||
|
"align": FormatAction(),
|
||||||
|
"alpha": FormatAction(style: (param, baseStyle) {
|
||||||
|
if (param == null || !param.startsWith("#")) return baseStyle;
|
||||||
|
final alpha = int.tryParse(param.substring(1), radix: 16);
|
||||||
|
if (alpha == null) return baseStyle;
|
||||||
|
return baseStyle.copyWith(color: baseStyle.color?.withAlpha(alpha));
|
||||||
|
}),
|
||||||
|
"color": FormatAction(style: (param, baseStyle) {
|
||||||
|
if (param == null) return baseStyle;
|
||||||
|
final color = tryParseColor(param);
|
||||||
|
if (color == null) return baseStyle;
|
||||||
|
return baseStyle.copyWith(color: color);
|
||||||
|
}),
|
||||||
|
"b": FormatAction(style: (param, baseStyle) => baseStyle.copyWith(fontWeight: FontWeight.bold)),
|
||||||
|
"br": FormatAction(transform: (text, param) => "\n$text"),
|
||||||
|
"i": FormatAction(style: (param, baseStyle) => baseStyle.copyWith(fontStyle: FontStyle.italic)),
|
||||||
|
"cspace": FormatAction(),
|
||||||
|
"font": FormatAction(),
|
||||||
|
"indent": FormatAction(),
|
||||||
|
"line-height": FormatAction(),
|
||||||
|
"line-indent": FormatAction(),
|
||||||
|
"link": FormatAction(),
|
||||||
|
"lowercase": FormatAction(transform: (input, parameter) => input.toLowerCase(),),
|
||||||
|
"uppercase": FormatAction(transform: (input, parameter) => input.toUpperCase(),),
|
||||||
|
"smallcaps": FormatAction(),
|
||||||
|
"margin": FormatAction(),
|
||||||
|
"mark": FormatAction(style: (param, baseStyle) {
|
||||||
|
if (param == null) return baseStyle;
|
||||||
|
final color = tryParseColor(param);
|
||||||
|
if (color == null) return baseStyle;
|
||||||
|
return baseStyle.copyWith(backgroundColor: color);
|
||||||
|
}),
|
||||||
|
"mspace": FormatAction(),
|
||||||
|
"noparse": FormatAction(),
|
||||||
|
"nobr": FormatAction(),
|
||||||
|
"page": FormatAction(),
|
||||||
|
"pos": FormatAction(),
|
||||||
|
"size": FormatAction(
|
||||||
|
style: (param, baseStyle) {
|
||||||
|
if (param == null) return baseStyle;
|
||||||
|
if (param.endsWith("%")) {
|
||||||
|
final percentage = int.tryParse(param.replaceAll("%", ""));
|
||||||
|
if (percentage == null || percentage <= 0) return baseStyle;
|
||||||
|
final baseSize = baseStyle.fontSize ?? 12;
|
||||||
|
return baseStyle.copyWith(fontSize: baseSize * (percentage / 100));
|
||||||
|
} else {
|
||||||
|
final size = num.tryParse(param);
|
||||||
|
if (size == null || size <= 0) return baseStyle;
|
||||||
|
return baseStyle.copyWith(fontSize: size.toDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"space": FormatAction(),
|
||||||
|
"sprite": FormatAction(),
|
||||||
|
"s": FormatAction(style: (param, baseStyle) => baseStyle.copyWith(decoration: TextDecoration.lineThrough)),
|
||||||
|
"u": FormatAction(style: (param, baseStyle) => baseStyle.copyWith(decoration: TextDecoration.underline)),
|
||||||
|
"style": FormatAction(),
|
||||||
|
"sub": FormatAction(),
|
||||||
|
"sup": FormatAction(),
|
||||||
|
"voffset": FormatAction(),
|
||||||
|
"width": FormatAction(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String parameter;
|
||||||
|
final bool isAdditive;
|
||||||
|
|
||||||
|
const FormatData({required this.name, required this.parameter, required this.isAdditive});
|
||||||
|
|
||||||
|
factory FormatData.parse(String text) {
|
||||||
|
if (text.contains("/")) return FormatData(name: text.replaceAll("/", ""), parameter: "", isAdditive: false);
|
||||||
|
final sepIdx = text.indexOf("=");
|
||||||
|
if (sepIdx == -1) {
|
||||||
|
return FormatData(name: text, parameter: "", isAdditive: true);
|
||||||
|
} else {
|
||||||
|
return FormatData(
|
||||||
|
name: text.substring(0, sepIdx).trim().toLowerCase(),
|
||||||
|
parameter: text.substring(sepIdx + 1, text.length).trim().toLowerCase(),
|
||||||
|
isAdditive: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
factory FormatData.unformatted() => const FormatData(name: "", parameter: "", isAdditive: false);
|
||||||
|
|
||||||
|
bool get isUnformatted => name.isEmpty && parameter.isEmpty && !isAdditive;
|
||||||
|
|
||||||
|
bool get isValid => _richTextTags.containsKey(name);
|
||||||
|
|
||||||
|
String? apply(String? text) => text == null ? null : _richTextTags[name]?.transform?.call(text, parameter);
|
||||||
|
|
||||||
|
TextStyle style() => _richTextTags[name]?.style?.call(parameter, const TextStyle()) ?? const TextStyle();
|
||||||
|
}
|
47
lib/widgets/formatted_text.dart
Normal file
47
lib/widgets/formatted_text.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:contacts_plus_plus/string_formatter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FormattedText extends StatelessWidget {
|
||||||
|
const FormattedText(this.formatTree, {
|
||||||
|
this.style,
|
||||||
|
this.textAlign,
|
||||||
|
this.overflow,
|
||||||
|
this.softWrap,
|
||||||
|
this.maxLines,
|
||||||
|
super.key
|
||||||
|
});
|
||||||
|
|
||||||
|
final FormatNode formatTree;
|
||||||
|
final TextStyle? style;
|
||||||
|
final TextAlign? textAlign;
|
||||||
|
final TextOverflow? overflow;
|
||||||
|
final bool? softWrap;
|
||||||
|
final int? maxLines;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (formatTree.isUnformatted) {
|
||||||
|
return Text(
|
||||||
|
formatTree.text,
|
||||||
|
style: style,
|
||||||
|
textAlign: textAlign,
|
||||||
|
overflow: overflow,
|
||||||
|
softWrap: softWrap,
|
||||||
|
maxLines: maxLines,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return RichText(
|
||||||
|
text: formatTree.toTextSpan(
|
||||||
|
baseStyle: style ?? Theme
|
||||||
|
.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
),
|
||||||
|
textAlign: textAlign ?? TextAlign.start,
|
||||||
|
overflow: overflow ?? TextOverflow.clip,
|
||||||
|
softWrap: softWrap ?? true,
|
||||||
|
maxLines: maxLines,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ import 'package:contacts_plus_plus/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:contacts_plus_plus/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/models/photo_asset.dart';
|
import 'package:contacts_plus_plus/models/photo_asset.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:contacts_plus_plus/models/message.dart';
|
||||||
|
import 'package:contacts_plus_plus/string_formatter.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart';
|
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:full_screen_image/full_screen_image.dart';
|
import 'package:full_screen_image/full_screen_image.dart';
|
||||||
|
@ -18,7 +20,6 @@ class MessageAsset extends StatelessWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
final DateFormat _dateFormat = DateFormat.Hm();
|
final DateFormat _dateFormat = DateFormat.Hm();
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final content = jsonDecode(message.content);
|
final content = jsonDecode(message.content);
|
||||||
|
@ -26,7 +27,7 @@ class MessageAsset extends StatelessWidget {
|
||||||
try {
|
try {
|
||||||
photoAsset = PhotoAsset.fromTags((content["tags"] as List).map((e) => "$e").toList());
|
photoAsset = PhotoAsset.fromTags((content["tags"] as List).map((e) => "$e").toList());
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
final formattedName = FormatNode.fromText(content["name"]);
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
|
@ -61,7 +62,17 @@ class MessageAsset extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text("${content["name"]}", maxLines: null, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.white60),)),
|
Expanded(
|
||||||
|
child: FormattedText(
|
||||||
|
formattedName,
|
||||||
|
maxLines: null,
|
||||||
|
style: Theme
|
||||||
|
.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall
|
||||||
|
?.copyWith(color: Colors.white60),
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:contacts_plus_plus/models/message.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/message_asset.dart';
|
import 'package:contacts_plus_plus/widgets/messages/message_asset.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/message_audio_player.dart';
|
import 'package:contacts_plus_plus/widgets/messages/message_audio_player.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/message_session_invite.dart';
|
import 'package:contacts_plus_plus/widgets/messages/message_session_invite.dart';
|
||||||
|
@ -6,6 +7,9 @@ import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
// The way these classes are laid out is pretty unclean, there's a lot of stuff that's shared between the different
|
||||||
|
// subwidgets with a lot of room for deduplication. Should probably redo this some day.
|
||||||
|
|
||||||
class MyMessageBubble extends StatelessWidget {
|
class MyMessageBubble extends StatelessWidget {
|
||||||
MyMessageBubble({required this.message, super.key});
|
MyMessageBubble({required this.message, super.key});
|
||||||
|
|
||||||
|
@ -79,8 +83,8 @@ class MyMessageBubble extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
FormattedText(
|
||||||
message.content,
|
message.formattedContent,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
style: Theme
|
style: Theme
|
||||||
|
@ -148,7 +152,6 @@ class OtherMessageBubble extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var content = message.content;
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case MessageType.sessionInvite:
|
case MessageType.sessionInvite:
|
||||||
return Row(
|
return Row(
|
||||||
|
@ -193,7 +196,6 @@ class OtherMessageBubble extends StatelessWidget {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
case MessageType.unknown:
|
case MessageType.unknown:
|
||||||
rawText:
|
|
||||||
case MessageType.text:
|
case MessageType.text:
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -214,8 +216,8 @@ class OtherMessageBubble extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
FormattedText(
|
||||||
content,
|
message.formattedContent,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
style: Theme
|
style: Theme
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:contacts_plus_plus/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:contacts_plus_plus/models/message.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
|
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart';
|
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart';
|
||||||
|
@ -38,7 +39,7 @@ class MessageSessionInvite extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(sessionInfo.name, maxLines: null, softWrap: true, style: Theme.of(context).textTheme.titleMedium,),
|
child: FormattedText(sessionInfo.formattedName, maxLines: null, softWrap: true, style: Theme.of(context).textTheme.titleMedium,),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
|
|
@ -126,9 +126,9 @@ class _MessagesListState extends State<MessagesList> {
|
||||||
builder: (context, mClient, _) {
|
builder: (context, mClient, _) {
|
||||||
final cache = mClient.getUserMessageCache(widget.friend.id);
|
final cache = mClient.getUserMessageCache(widget.friend.id);
|
||||||
if (cache == null) {
|
if (cache == null) {
|
||||||
return Column(
|
return const Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: const [
|
children: [
|
||||||
LinearProgressIndicator()
|
LinearProgressIndicator()
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -30,8 +31,10 @@ class SessionPopup extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
Text(session.name, style: Theme.of(context).textTheme.titleMedium),
|
FormattedText(session.formattedName, style: Theme.of(context).textTheme.titleMedium),
|
||||||
Text(session.description.isEmpty ? "No description." : session.description, style: Theme.of(context).textTheme.labelMedium),
|
session.formattedDescription.isEmpty
|
||||||
|
? const Text("No description")
|
||||||
|
: FormattedText(session.formattedDescription, style: Theme.of(context).textTheme.labelMedium),
|
||||||
Text("Tags: ${session.tags.isEmpty ? "None" : session.tags.join(", ")}",
|
Text("Tags: ${session.tags.isEmpty ? "None" : session.tags.join(", ")}",
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
|
@ -62,11 +65,11 @@ class SessionPopup extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
) else Expanded(
|
) else const Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: const [
|
children: [
|
||||||
Icon(Icons.person_remove_alt_1_rounded),
|
Icon(Icons.person_remove_alt_1_rounded),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
|
@ -86,11 +89,11 @@ class SessionPopup extends StatelessWidget {
|
||||||
placeholder: (context, url) {
|
placeholder: (context, url) {
|
||||||
return const CircularProgressIndicator();
|
return const CircularProgressIndicator();
|
||||||
},
|
},
|
||||||
errorWidget: (context, error, what) => Column(
|
errorWidget: (context, error, what) => const Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: const [
|
children: [
|
||||||
Icon(Icons.no_photography),
|
Icon(Icons.no_photography),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
|
|
|
@ -81,6 +81,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.1"
|
version: "1.17.1"
|
||||||
|
color:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: color
|
||||||
|
sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.1+1
|
version: 1.2.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0'
|
sdk: '>=3.0.0'
|
||||||
|
@ -55,6 +55,7 @@ dependencies:
|
||||||
provider: ^6.0.5
|
provider: ^6.0.5
|
||||||
full_screen_image: ^2.0.0
|
full_screen_image: ^2.0.0
|
||||||
photo_view: ^0.14.0
|
photo_view: ^0.14.0
|
||||||
|
color: ^3.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue