Skip to content

Commit aaae03b

Browse files
Merge branch 'zulip:main' into DmBackground
2 parents 3ba4bd0 + e0df0ed commit aaae03b

30 files changed

+766
-159
lines changed

.mailmap

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
1616
1717
Lalit Kumar Singh <[email protected]>
18-
Rajesh Malviya <[email protected]>
18+
1919
2020

2121
# The goal when editing this file is to group all of a given person's

lib/model/autocomplete.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
449449
required PerAccountStore store,
450450
required Narrow narrow,
451451
}) {
452-
return store.users.values.toList()
452+
return store.allUsers.toList()
453453
..sort(_comparator(store: store, narrow: narrow));
454454
}
455455

@@ -622,13 +622,13 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
622622
if (tryOption(WildcardMentionOption.all)) break all;
623623
if (tryOption(WildcardMentionOption.everyone)) break all;
624624
if (isComposingChannelMessage) {
625-
final isChannelWildcardAvailable = store.account.zulipFeatureLevel >= 247; // TODO(server-9)
625+
final isChannelWildcardAvailable = store.zulipFeatureLevel >= 247; // TODO(server-9)
626626
if (isChannelWildcardAvailable && tryOption(WildcardMentionOption.channel)) break all;
627627
if (tryOption(WildcardMentionOption.stream)) break all;
628628
}
629629
}
630630

631-
final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(server-8)
631+
final isTopicWildcardAvailable = store.zulipFeatureLevel >= 224; // TODO(server-8)
632632
if (isComposingChannelMessage && isTopicWildcardAvailable) {
633633
tryOption(WildcardMentionOption.topic);
634634
}

lib/model/compose.dart

+11-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import '../generated/l10n/zulip_localizations.dart';
55
import 'internal_link.dart';
66
import 'narrow.dart';
77
import 'store.dart';
8+
import 'user.dart';
89

910
/// The available user wildcard mention options,
1011
/// known to the server as [canonicalString].
@@ -127,11 +128,12 @@ String wrapWithBacktickFence({required String content, String? infoString}) {
127128
/// An @-mention of an individual user, like @**Chris Bobbe|13313**.
128129
///
129130
/// To omit the user ID part ("|13313") whenever the name part is unambiguous,
130-
/// pass a Map of all users we know about. This means accepting a linear scan
131+
/// pass the full UserStore. This means accepting a linear scan
131132
/// through all users; avoid it in performance-sensitive codepaths.
132-
String userMention(User user, {bool silent = false, Map<int, User>? users}) {
133+
String userMention(User user, {bool silent = false, UserStore? users}) {
133134
bool includeUserId = users == null
134-
|| users.values.where((u) => u.fullName == user.fullName).take(2).length == 2;
135+
|| users.allUsers.where((u) => u.fullName == user.fullName)
136+
.take(2).length == 2;
135137

136138
return '@${silent ? '_' : ''}**${user.fullName}${includeUserId ? '|${user.userId}' : ''}**';
137139
}
@@ -140,8 +142,8 @@ String userMention(User user, {bool silent = false, Map<int, User>? users}) {
140142
String wildcardMention(WildcardMentionOption wildcardOption, {
141143
required PerAccountStore store,
142144
}) {
143-
final isChannelWildcardAvailable = store.account.zulipFeatureLevel >= 247; // TODO(server-9)
144-
final isTopicWildcardAvailable = store.account.zulipFeatureLevel >= 224; // TODO(server-8)
145+
final isChannelWildcardAvailable = store.zulipFeatureLevel >= 247; // TODO(server-9)
146+
final isTopicWildcardAvailable = store.zulipFeatureLevel >= 224; // TODO(server-8)
145147

146148
String name = wildcardOption.canonicalString;
147149
switch (wildcardOption) {
@@ -188,8 +190,8 @@ String quoteAndReplyPlaceholder(
188190
PerAccountStore store, {
189191
required Message message,
190192
}) {
191-
final sender = store.users[message.senderId];
192-
assert(sender != null);
193+
final sender = store.getUser(message.senderId);
194+
assert(sender != null); // TODO(#716): should use `store.senderDisplayName`
193195
final url = narrowLink(store,
194196
SendableNarrow.ofMessage(message, selfUserId: store.selfUserId),
195197
nearMessageId: message.id);
@@ -210,8 +212,8 @@ String quoteAndReply(PerAccountStore store, {
210212
required Message message,
211213
required String rawContent,
212214
}) {
213-
final sender = store.users[message.senderId];
214-
assert(sender != null);
215+
final sender = store.getUser(message.senderId);
216+
assert(sender != null); // TODO(#716): should use `store.senderDisplayName`
215217
final url = narrowLink(store,
216218
SendableNarrow.ofMessage(message, selfUserId: store.selfUserId),
217219
nearMessageId: message.id);

lib/model/content.dart

+163
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,58 @@ class EmbedVideoNode extends BlockContentNode {
504504
}
505505
}
506506

507+
// See:
508+
// https://ogp.me/
509+
// https://oembed.com/
510+
// https://zulip.com/help/image-video-and-website-previews#configure-whether-website-previews-are-shown
511+
class WebsitePreviewNode extends BlockContentNode {
512+
const WebsitePreviewNode({
513+
super.debugHtmlNode,
514+
required this.hrefUrl,
515+
required this.imageSrcUrl,
516+
required this.title,
517+
required this.description,
518+
});
519+
520+
/// The URL from which this preview data was retrieved.
521+
final String hrefUrl;
522+
523+
/// The image URL representing the webpage, content value
524+
/// of `og:image` HTML meta property.
525+
final String imageSrcUrl;
526+
527+
/// Represents the webpage title, derived from either
528+
/// the content of the `og:title` HTML meta property or
529+
/// the <title> HTML element.
530+
final String? title;
531+
532+
/// Description about the webpage, content value of
533+
/// `og:description` HTML meta property.
534+
final String? description;
535+
536+
@override
537+
bool operator ==(Object other) {
538+
return other is WebsitePreviewNode
539+
&& other.hrefUrl == hrefUrl
540+
&& other.imageSrcUrl == imageSrcUrl
541+
&& other.title == title
542+
&& other.description == description;
543+
}
544+
545+
@override
546+
int get hashCode =>
547+
Object.hash('WebsitePreviewNode', hrefUrl, imageSrcUrl, title, description);
548+
549+
@override
550+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
551+
super.debugFillProperties(properties);
552+
properties.add(StringProperty('hrefUrl', hrefUrl));
553+
properties.add(StringProperty('imageSrcUrl', imageSrcUrl));
554+
properties.add(StringProperty('title', title));
555+
properties.add(StringProperty('description', description));
556+
}
557+
}
558+
507559
class TableNode extends BlockContentNode {
508560
const TableNode({super.debugHtmlNode, required this.rows});
509561

@@ -1339,6 +1391,113 @@ class _ZulipContentParser {
13391391
return EmbedVideoNode(hrefUrl: href, previewImageSrcUrl: imgSrc, debugHtmlNode: debugHtmlNode);
13401392
}
13411393

1394+
static final _websitePreviewImageSrcRegexp = RegExp(r'background-image: url\(("?)(.+?)\1\)');
1395+
1396+
BlockContentNode parseWebsitePreviewNode(dom.Element divElement) {
1397+
assert(divElement.localName == 'div'
1398+
&& divElement.className == 'message_embed');
1399+
1400+
final debugHtmlNode = kDebugMode ? divElement : null;
1401+
final result = () {
1402+
if (divElement.nodes case [
1403+
dom.Element(
1404+
localName: 'a',
1405+
className: 'message_embed_image',
1406+
attributes: {
1407+
'href': final String imageHref,
1408+
'style': final String imageStyleAttr,
1409+
},
1410+
nodes: []),
1411+
dom.Element(
1412+
localName: 'div',
1413+
className: 'data-container',
1414+
nodes: [...]) && final dataContainer,
1415+
]) {
1416+
final match = _websitePreviewImageSrcRegexp.firstMatch(imageStyleAttr);
1417+
if (match == null) return null;
1418+
final imageSrcUrl = match.group(2);
1419+
if (imageSrcUrl == null) return null;
1420+
1421+
String? parseTitle(dom.Element element) {
1422+
assert(element.localName == 'div' &&
1423+
element.className == 'message_embed_title');
1424+
if (element.nodes case [
1425+
dom.Element(localName: 'a', className: '') && final child,
1426+
]) {
1427+
final titleHref = child.attributes['href'];
1428+
// Make sure both image hyperlink and title hyperlink are same.
1429+
if (imageHref != titleHref) return null;
1430+
1431+
if (child.nodes case [dom.Text(text: final title)]) {
1432+
return title;
1433+
}
1434+
}
1435+
return null;
1436+
}
1437+
1438+
String? parseDescription(dom.Element element) {
1439+
assert(element.localName == 'div' &&
1440+
element.className == 'message_embed_description');
1441+
if (element.nodes case [dom.Text(text: final description)]) {
1442+
return description;
1443+
}
1444+
return null;
1445+
}
1446+
1447+
String? title, description;
1448+
switch (dataContainer.nodes) {
1449+
case [
1450+
dom.Element(
1451+
localName: 'div',
1452+
className: 'message_embed_title') && final first,
1453+
dom.Element(
1454+
localName: 'div',
1455+
className: 'message_embed_description') && final second,
1456+
]:
1457+
title = parseTitle(first);
1458+
if (title == null) return null;
1459+
description = parseDescription(second);
1460+
if (description == null) return null;
1461+
1462+
case [dom.Element(localName: 'div') && final single]:
1463+
switch (single.className) {
1464+
case 'message_embed_title':
1465+
title = parseTitle(single);
1466+
if (title == null) return null;
1467+
1468+
case 'message_embed_description':
1469+
description = parseDescription(single);
1470+
if (description == null) return null;
1471+
1472+
default:
1473+
return null;
1474+
}
1475+
1476+
case []:
1477+
// Server generates an empty `<div class="data-container"></div>`
1478+
// if website HTML has neither title (derived from
1479+
// `og:title` or `<title>…</title>`) nor description (derived from
1480+
// `og:description`).
1481+
break;
1482+
1483+
default:
1484+
return null;
1485+
}
1486+
1487+
return WebsitePreviewNode(
1488+
hrefUrl: imageHref,
1489+
imageSrcUrl: imageSrcUrl,
1490+
title: title,
1491+
description: description,
1492+
debugHtmlNode: debugHtmlNode);
1493+
} else {
1494+
return null;
1495+
}
1496+
}();
1497+
1498+
return result ?? UnimplementedBlockContentNode(htmlNode: divElement);
1499+
}
1500+
13421501
BlockContentNode parseTableContent(dom.Element tableElement) {
13431502
assert(tableElement.localName == 'table'
13441503
&& tableElement.className.isEmpty);
@@ -1583,6 +1742,10 @@ class _ZulipContentParser {
15831742
}
15841743
}
15851744

1745+
if (localName == 'div' && className == 'message_embed') {
1746+
return parseWebsitePreviewNode(element);
1747+
}
1748+
15861749
// TODO more types of node
15871750
return UnimplementedBlockContentNode(htmlNode: node);
15881751
}

lib/model/internal_link.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ String? decodeHashComponent(String str) {
6060
Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) {
6161
// TODO(server-7)
6262
final apiNarrow = resolveApiNarrowForServer(
63-
narrow.apiEncode(), store.connection.zulipFeatureLevel!);
63+
narrow.apiEncode(), store.zulipFeatureLevel);
6464
final fragment = StringBuffer('narrow');
6565
for (ApiNarrowElement element in apiNarrow) {
6666
fragment.write('/');

0 commit comments

Comments
 (0)