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
69 changes: 37 additions & 32 deletions lib/model/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -524,19 +524,19 @@ class MathBlockNode extends MathNode implements BlockContentNode {
});
}

class ImageNodeList extends BlockContentNode {
const ImageNodeList(this.images, {super.debugHtmlNode});
class ImagePreviewNodeList extends BlockContentNode {
const ImagePreviewNodeList(this.imagePreviews, {super.debugHtmlNode});

final List<ImageNode> images;
final List<ImagePreviewNode> imagePreviews;

@override
List<DiagnosticsNode> debugDescribeChildren() {
return images.map((node) => node.toDiagnosticsNode()).toList();
return imagePreviews.map((node) => node.toDiagnosticsNode()).toList();
}
}

class ImageNode extends BlockContentNode {
const ImageNode({
class ImagePreviewNode extends BlockContentNode {
const ImagePreviewNode({
super.debugHtmlNode,
required this.srcUrl,
required this.thumbnailUrl,
Expand Down Expand Up @@ -574,7 +574,7 @@ class ImageNode extends BlockContentNode {

@override
bool operator ==(Object other) {
return other is ImageNode
return other is ImagePreviewNode
&& other.srcUrl == srcUrl
&& other.thumbnailUrl == thumbnailUrl
&& other.loading == loading
Expand All @@ -583,7 +583,7 @@ class ImageNode extends BlockContentNode {
}

@override
int get hashCode => Object.hash('ImageNode',
int get hashCode => Object.hash('ImagePreviewNode',
srcUrl, thumbnailUrl, loading, originalWidth, originalHeight);

@override
Expand Down Expand Up @@ -1368,7 +1368,7 @@ class _ZulipContentParser {

static final _imageDimensionsRegExp = RegExp(r'^(\d+)x(\d+)$');

BlockContentNode parseImageNode(dom.Element divElement) {
BlockContentNode parseImagePreviewNode(dom.Element divElement) {
final elements = () {
assert(divElement.localName == 'div'
&& divElement.className == 'message_inline_image');
Expand Down Expand Up @@ -1397,7 +1397,7 @@ class _ZulipContentParser {
return UnimplementedBlockContentNode(htmlNode: divElement);
}
if (imgElement.className == 'image-loading-placeholder') {
return ImageNode(
return ImagePreviewNode(
srcUrl: href,
thumbnailUrl: null,
loading: true,
Expand All @@ -1417,9 +1417,14 @@ class _ZulipContentParser {
thumbnailUrl = src;
} else if (src.startsWith('/external_content/')
|| src.startsWith('https://uploads.zulipusercontent.net/')) {
// This image preview uses camo, which still happens on current servers
// (2025-10); discussion:
// https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/documenting.20inline.20images/near/2279235
srcUrl = src;
thumbnailUrl = null;
} else if (href == src) {
// Probably generated by a server before the thumbnailing feature landed:
// https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/documenting.20inline.20images/near/2279234
srcUrl = src;
thumbnailUrl = null;
} else {
Expand All @@ -1445,7 +1450,7 @@ class _ZulipContentParser {
}
}

return ImageNode(
return ImagePreviewNode(
srcUrl: srcUrl,
thumbnailUrl: thumbnailUrl,
loading: false,
Expand Down Expand Up @@ -1870,7 +1875,7 @@ class _ZulipContentParser {
}

if (localName == 'div' && className == 'message_inline_image') {
return parseImageNode(element);
return parseImagePreviewNode(element);
}

if (localName == 'div') {
Expand Down Expand Up @@ -1923,10 +1928,10 @@ class _ZulipContentParser {
List<BlockContentNode> parseImplicitParagraphBlockContentList(dom.NodeList nodes) {
final List<BlockContentNode> result = [];

List<ImageNode> imageNodes = [];
void consumeImageNodes() {
result.add(ImageNodeList(imageNodes));
imageNodes = [];
List<ImagePreviewNode> imagePreviewNodes = [];
void consumeImagePreviewNodes() {
result.add(ImagePreviewNodeList(imagePreviewNodes));
imagePreviewNodes = [];
}

final List<dom.Node> currentParagraph = [];
Expand All @@ -1948,14 +1953,14 @@ class _ZulipContentParser {
if (node case dom.Element(localName: 'p', className: '', nodes: [
dom.Element(localName: 'span', className: 'katex-display'), ...])) {
if (currentParagraph.isNotEmpty) consumeParagraph();
if (imageNodes.isNotEmpty) consumeImageNodes();
if (imagePreviewNodes.isNotEmpty) consumeImagePreviewNodes();
parseMathBlocks(node.nodes, result);
continue;
}

if (_isPossibleInlineNode(node)) {
if (imageNodes.isNotEmpty) {
consumeImageNodes();
if (imagePreviewNodes.isNotEmpty) {
consumeImagePreviewNodes();
// In a context where paragraphs are implicit it should be impossible
// to have more paragraph content after image previews.
result.add(UnimplementedBlockContentNode(htmlNode: node));
Expand All @@ -1966,15 +1971,15 @@ class _ZulipContentParser {
}
if (currentParagraph.isNotEmpty) consumeParagraph();
final block = parseBlockContent(node);
if (block is ImageNode) {
imageNodes.add(block);
if (block is ImagePreviewNode) {
imagePreviewNodes.add(block);
continue;
}
if (imageNodes.isNotEmpty) consumeImageNodes();
if (imagePreviewNodes.isNotEmpty) consumeImagePreviewNodes();
result.add(block);
}
if (currentParagraph.isNotEmpty) consumeParagraph();
if (imageNodes.isNotEmpty) consumeImageNodes();
if (imagePreviewNodes.isNotEmpty) consumeImagePreviewNodes();
return result;
}

Expand All @@ -1983,10 +1988,10 @@ class _ZulipContentParser {
List<BlockContentNode> parseBlockContentList(dom.NodeList nodes) {
final List<BlockContentNode> result = [];

List<ImageNode> imageNodes = [];
void consumeImageNodes() {
result.add(ImageNodeList(imageNodes));
imageNodes = [];
List<ImagePreviewNode> imagePreviewNodes = [];
void consumeImagePreviewNodes() {
result.add(ImagePreviewNodeList(imagePreviewNodes));
imagePreviewNodes = [];
}

for (final node in nodes) {
Expand All @@ -2001,20 +2006,20 @@ class _ZulipContentParser {
// handle it explicitly here.
if (node case dom.Element(localName: 'p', className: '', nodes: [
dom.Element(localName: 'span', className: 'katex-display'), ...])) {
if (imageNodes.isNotEmpty) consumeImageNodes();
if (imagePreviewNodes.isNotEmpty) consumeImagePreviewNodes();
parseMathBlocks(node.nodes, result);
continue;
}

final block = parseBlockContent(node);
if (block is ImageNode) {
imageNodes.add(block);
if (block is ImagePreviewNode) {
imagePreviewNodes.add(block);
continue;
}
if (imageNodes.isNotEmpty) consumeImageNodes();
if (imagePreviewNodes.isNotEmpty) consumeImagePreviewNodes();
result.add(block);
}
if (imageNodes.isNotEmpty) consumeImageNodes();
if (imagePreviewNodes.isNotEmpty) consumeImagePreviewNodes();
return result;
}

Expand Down
24 changes: 12 additions & 12 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,13 @@ class BlockContentList extends StatelessWidget {
SpoilerNode() => Spoiler(node: node),
CodeBlockNode() => CodeBlock(node: node),
MathBlockNode() => MathBlock(node: node),
ImageNodeList() => MessageImageList(node: node),
ImageNode() => (){
ImagePreviewNodeList() => MessageImagePreviewList(node: node),
ImagePreviewNode() => (){
assert(false,
"[ImageNode] not allowed in [BlockContentList]. "
"It should be wrapped in [ImageNodeList]."
"[ImagePreviewNode] not allowed in [BlockContentList]. "
"It should be wrapped in [ImagePreviewNodeList]."
);
return MessageImage(node: node);
return MessageImagePreview(node: node);
}(),
InlineVideoNode() => MessageInlineVideo(node: node),
EmbedVideoNode() => MessageEmbedVideo(node: node),
Expand Down Expand Up @@ -614,22 +614,22 @@ class _SpoilerState extends State<Spoiler> with TickerProviderStateMixin {
}
}

class MessageImageList extends StatelessWidget {
const MessageImageList({super.key, required this.node});
class MessageImagePreviewList extends StatelessWidget {
const MessageImagePreviewList({super.key, required this.node});

final ImageNodeList node;
final ImagePreviewNodeList node;

@override
Widget build(BuildContext context) {
return Wrap(
children: node.images.map((imageNode) => MessageImage(node: imageNode)).toList());
children: node.imagePreviews.map((node) => MessageImagePreview(node: node)).toList());
}
}

class MessageImage extends StatelessWidget {
const MessageImage({super.key, required this.node});
class MessageImagePreview extends StatelessWidget {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: git grep -w MessageImage finds one straggler reference in a comment

const MessageImagePreview({super.key, required this.node});

final ImageNode node;
final ImagePreviewNode node;

@override
Widget build(BuildContext context) {
Expand Down
4 changes: 2 additions & 2 deletions lib/widgets/lightbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class _LightboxHeroTag {
required this.src,
});

/// The [BuildContext] for the [MessageImage] being expanded into the lightbox.
/// The [BuildContext] for the [MessageImagePreview] being expanded into the lightbox.
///
/// In particular this prevents hero animations between
/// different message lists that happen to have the same message.
Expand All @@ -45,7 +45,7 @@ class _LightboxHeroTag {
///
/// This ensures the animation only occurs between matching images, even if
/// the message was edited before navigating back to the message list
/// so that the original [MessageImage] has been replaced in the tree
/// so that the original [MessageImagePreview] has been replaced in the tree
/// by a different image.
final Uri src;

Expand Down
Loading