Skip to content

Commit

Permalink
Merge branch 'main' into Komyyy/user_local_time
Browse files Browse the repository at this point in the history
  • Loading branch information
Komyyy committed Feb 17, 2025
2 parents 878dec0 + 44df81f commit f21925a
Show file tree
Hide file tree
Showing 7 changed files with 674 additions and 144 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ You can also [try out this beta app][beta].
Contributions to this app are welcome.

If you're looking to participate in Google Summer of Code with Zulip,
this is one of the projects we're [accepting GSoC 2024 applications][]
this is one of the projects we intend to accept [GSoC 2025 applications][gsoc]
for.

[accepting GSoC 2024 applications]: https://zulip.readthedocs.io/en/latest/outreach/gsoc.html#mobile-app
[gsoc]: https://zulip.readthedocs.io/en/latest/outreach/gsoc.html#mobile-app


### Picking an issue to work on
Expand Down
65 changes: 48 additions & 17 deletions lib/example/sticky_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,54 +125,75 @@ class ExampleVerticalDouble extends StatelessWidget {
super.key,
required this.title,
// this.reverse = false,
// this.headerDirection = AxisDirection.down,
}); // : assert(axisDirectionToAxis(headerDirection) == Axis.vertical);
required this.headerPlacement,
});

final String title;
// final bool reverse;
// final AxisDirection headerDirection;
final HeaderPlacement headerPlacement;

@override
Widget build(BuildContext context) {
const centerSliverKey = ValueKey('center sliver');
const numSections = 100;
const numSections = 4;
const numBottomSections = 2;
const numTopSections = numSections - numBottomSections;
const numPerSection = 10;

final headerAtBottom = switch (headerPlacement) {
HeaderPlacement.scrollingStart => false,
HeaderPlacement.scrollingEnd => true,
};

// Choose the "center" sliver so that the sliver which might need to paint
// a header overflowing the other header is the sliver that paints last.
final centerKey = headerAtBottom ?
const ValueKey('bottom') : const ValueKey('top');

// This is a side effect of our choice of centerKey.
final topSliverGrowsUpward = headerAtBottom;

return Scaffold(
appBar: AppBar(title: Text(title)),
body: CustomScrollView(
semanticChildCount: numSections,
anchor: 0.5,
center: centerSliverKey,
center: centerKey,
slivers: [
SliverStickyHeaderList(
headerPlacement: HeaderPlacement.scrollingStart,
key: const ValueKey('top'),
headerPlacement: headerPlacement,
delegate: SliverChildBuilderDelegate(
childCount: numSections - numBottomSections,
(context, i) {
final ii = i + numBottomSections;
final ii = numBottomSections
+ (topSliverGrowsUpward ? i : numTopSections - 1 - i);
return StickyHeaderItem(
allowOverflow: true,
header: WideHeader(i: ii),
child: Column(
verticalDirection: headerAtBottom
? VerticalDirection.up : VerticalDirection.down,
children: List.generate(numPerSection + 1, (j) {
if (j == 0) return WideHeader(i: ii);
return WideItem(i: ii, j: j-1);
})));
})),
SliverStickyHeaderList(
key: centerSliverKey,
headerPlacement: HeaderPlacement.scrollingStart,
key: const ValueKey('bottom'),
headerPlacement: headerPlacement,
delegate: SliverChildBuilderDelegate(
childCount: numBottomSections,
(context, i) {
final ii = numBottomSections - 1 - i;
return StickyHeaderItem(
allowOverflow: true,
header: WideHeader(i: ii),
child: Column(
verticalDirection: headerAtBottom
? VerticalDirection.up : VerticalDirection.down,
children: List.generate(numPerSection + 1, (j) {
if (j == 0) return WideHeader(i: ii);
return WideItem(i: ii, j: j-1);
})));
if (j == 0) return WideHeader(i: ii);
return WideItem(i: ii, j: j-1);
})));
})),
]));
}
Expand All @@ -197,6 +218,7 @@ class WideHeader extends StatelessWidget {
return Material(
color: Theme.of(context).colorScheme.primaryContainer,
child: ListTile(
onTap: () {}, // nop, but non-null so the ink splash appears
title: Text("Section ${i + 1}",
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer))));
Expand All @@ -211,7 +233,9 @@ class WideItem extends StatelessWidget {

@override
Widget build(BuildContext context) {
return ListTile(title: Text("Item ${i + 1}.${j + 1}"));
return ListTile(
onTap: () {}, // nop, but non-null so the ink splash appears
title: Text("Item ${i + 1}.${j + 1}"));
}
}

Expand Down Expand Up @@ -318,8 +342,15 @@ class MainPage extends StatelessWidget {
];
final otherItems = [
_buildButton(context,
title: 'Double slivers',
page: ExampleVerticalDouble(title: 'Double slivers')),
title: 'Double slivers, headers at top',
page: ExampleVerticalDouble(
title: 'Double slivers, headers at top',
headerPlacement: HeaderPlacement.scrollingStart)),
_buildButton(context,
title: 'Double slivers, headers at bottom',
page: ExampleVerticalDouble(
title: 'Double slivers, headers at bottom',
headerPlacement: HeaderPlacement.scrollingEnd)),
];
return Scaffold(
appBar: AppBar(title: const Text('Sticky Headers example')),
Expand Down
101 changes: 79 additions & 22 deletions lib/model/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1055,13 +1055,6 @@ class _ZulipContentParser {
return inlineParser.parseBlockInline(nodes);
}

BlockContentNode parseMathBlock(dom.Element element) {
final debugHtmlNode = kDebugMode ? element : null;
final texSource = _parseMath(element, block: true);
if (texSource == null) return UnimplementedBlockContentNode(htmlNode: element);
return MathBlockNode(texSource: texSource, debugHtmlNode: debugHtmlNode);
}

BlockContentNode parseListNode(dom.Element element) {
ListStyle? listStyle;
switch (element.localName) {
Expand Down Expand Up @@ -1453,6 +1446,64 @@ class _ZulipContentParser {
return tableNode ?? UnimplementedBlockContentNode(htmlNode: tableElement);
}

void parseMathBlocks(dom.NodeList nodes, List<BlockContentNode> result) {
assert(nodes.isNotEmpty);
assert((() {
final first = nodes.first;
return first is dom.Element
&& first.localName == 'span'
&& first.className == 'katex-display';
})());

final firstChild = nodes.first as dom.Element;
final texSource = _parseMath(firstChild, block: true);
if (texSource != null) {
result.add(MathBlockNode(
texSource: texSource,
debugHtmlNode: kDebugMode ? firstChild : null));
} else {
result.add(UnimplementedBlockContentNode(htmlNode: firstChild));
}

// Skip further checks if there was only a single child.
if (nodes.length == 1) return;

// The case with the `<br>\n` can happen when at the end of a quote;
// it seems like a glitch in the server's Markdown processing,
// so hopefully there just aren't any further such glitches.
bool hasTrailingBreakNewline = false;
if (nodes case [..., dom.Element(localName: 'br'), dom.Text(text: '\n')]) {
hasTrailingBreakNewline = true;
}

final length = hasTrailingBreakNewline
? nodes.length - 2
: nodes.length;
for (int i = 1; i < length; i++) {
final child = nodes[i];
final debugHtmlNode = kDebugMode ? child : null;

// If there are multiple <span class="katex-display"> nodes in a <p>
// each node is interleaved by '\n\n'. Whitespaces are ignored in HTML
// on web but each node has `display: block`, which renders each node
// on a new line. Since the emitted MathBlockNode are BlockContentNode,
// we skip these newlines here to replicate the same behavior as on web.
if (child case dom.Text(text: '\n\n')) continue;

if (child case dom.Element(localName: 'span', className: 'katex-display')) {
final texSource = _parseMath(child, block: true);
if (texSource != null) {
result.add(MathBlockNode(
texSource: texSource,
debugHtmlNode: debugHtmlNode));
continue;
}
}

result.add(UnimplementedBlockContentNode(htmlNode: child));
}
}

BlockContentNode parseBlockContent(dom.Node node) {
final debugHtmlNode = kDebugMode ? node : null;
if (node is! dom.Element) {
Expand All @@ -1471,21 +1522,6 @@ class _ZulipContentParser {
}

if (localName == 'p' && className.isEmpty) {
// Oddly, the way a math block gets encoded in Zulip HTML is inside a <p>.
if (element.nodes case [dom.Element(localName: 'span') && var child, ...]) {
if (child.className == 'katex-display') {
if (element.nodes case [_]
|| [_, dom.Element(localName: 'br'),
dom.Text(text: "\n")]) {
// This might be too specific; we'll find out when we do #190.
// The case with the `<br>\n` can happen when at the end of a quote;
// it seems like a glitch in the server's Markdown processing,
// so hopefully there just aren't any further such glitches.
return parseMathBlock(child);
}
}
}

final parsed = parseBlockInline(element.nodes);
return ParagraphNode(debugHtmlNode: debugHtmlNode,
links: parsed.links,
Expand Down Expand Up @@ -1599,6 +1635,17 @@ class _ZulipContentParser {
for (final node in nodes) {
if (node is dom.Text && (node.text == '\n')) continue;

// Oddly, the way math blocks get encoded in Zulip HTML is inside a <p>.
// And there can be multiple math blocks inside the paragraph node, so
// handle it explicitly here.
if (node case dom.Element(localName: 'p', className: '', nodes: [
dom.Element(localName: 'span', className: 'katex-display'), ...])) {
if (currentParagraph.isNotEmpty) consumeParagraph();
if (imageNodes.isNotEmpty) consumeImageNodes();
parseMathBlocks(node.nodes, result);
continue;
}

if (_isPossibleInlineNode(node)) {
if (imageNodes.isNotEmpty) {
consumeImageNodes();
Expand Down Expand Up @@ -1642,6 +1689,16 @@ class _ZulipContentParser {
continue;
}

// Oddly, the way math blocks get encoded in Zulip HTML is inside a <p>.
// And there can be multiple math blocks inside the paragraph node, so
// handle it explicitly here.
if (node case dom.Element(localName: 'p', className: '', nodes: [
dom.Element(localName: 'span', className: 'katex-display'), ...])) {
if (imageNodes.isNotEmpty) consumeImageNodes();
parseMathBlocks(node.nodes, result);
continue;
}

final block = parseBlockContent(node);
if (block is ImageNode) {
imageNodes.add(block);
Expand Down
17 changes: 13 additions & 4 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_color_models/flutter_color_models.dart';
import 'package:intl/intl.dart';
import 'package:intl/intl.dart' hide TextDirection;

import '../api/model/model.dart';
import '../generated/l10n/zulip_localizations.dart';
Expand Down Expand Up @@ -1388,6 +1388,17 @@ class MessageWithPossibleSender extends StatelessWidget {
case MessageEditState.none:
}

Widget? star;
if (message.flags.contains(MessageFlag.starred)) {
final starOffset = switch (Directionality.of(context)) {
TextDirection.ltr => -2.0,
TextDirection.rtl => 2.0,
};
star = Transform.translate(
offset: Offset(starOffset, 0),
child: Icon(ZulipIcons.star_filled, size: 16, color: designVariables.star));
}

return GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () => showMessageActionSheet(context: context, message: message),
Expand Down Expand Up @@ -1419,9 +1430,7 @@ class MessageWithPossibleSender extends StatelessWidget {
context, 0.05, baseFontSize: 12))),
])),
SizedBox(width: 16,
child: message.flags.contains(MessageFlag.starred)
? Icon(ZulipIcons.star_filled, size: 16, color: designVariables.star)
: null),
child: star),
]),
])));
}
Expand Down
Loading

0 comments on commit f21925a

Please sign in to comment.