Skip to content

Commit 9ee8ccc

Browse files
committed
image [nfc]: Move RealmContentNetworkImage to new widgets/image.dart file
1 parent dc59d62 commit 9ee8ccc

File tree

12 files changed

+173
-154
lines changed

12 files changed

+173
-154
lines changed

lib/widgets/content.dart

Lines changed: 1 addition & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart';
77
import 'package:html/dom.dart' as dom;
88
import 'package:intl/intl.dart' as intl;
99

10-
import '../api/core.dart';
1110
import '../api/model/model.dart';
1211
import '../generated/l10n/zulip_localizations.dart';
1312
import '../model/content.dart';
@@ -16,6 +15,7 @@ import 'actions.dart';
1615
import 'code_block.dart';
1716
import 'dialog.dart';
1817
import 'icons.dart';
18+
import 'image.dart';
1919
import 'inset_shadow.dart';
2020
import 'katex.dart';
2121
import 'lightbox.dart';
@@ -1447,111 +1447,6 @@ void _launchUrl(BuildContext context, String urlString) async {
14471447
}
14481448
}
14491449

1450-
/// Like [Image.network], but includes [authHeader] if [src] is on-realm.
1451-
///
1452-
/// Use this to present image content in the ambient realm: avatars, images in
1453-
/// messages, etc. Must have a [PerAccountStoreWidget] ancestor.
1454-
///
1455-
/// If [src] is an on-realm URL (it has the same origin as the ambient
1456-
/// [Auth.realmUrl]), then an HTTP request to fetch the image will include the
1457-
/// user's [authHeader].
1458-
///
1459-
/// If [src] is off-realm (e.g., a Gravatar URL), no auth header will be sent.
1460-
///
1461-
/// The image will be cached according to the cache behavior of [Image.network],
1462-
/// which may mean the cache is shared between realms.
1463-
class RealmContentNetworkImage extends StatelessWidget {
1464-
const RealmContentNetworkImage(
1465-
this.src, {
1466-
super.key,
1467-
this.scale = 1.0,
1468-
this.frameBuilder,
1469-
this.loadingBuilder,
1470-
this.errorBuilder,
1471-
this.semanticLabel,
1472-
this.excludeFromSemantics = false,
1473-
this.width,
1474-
this.height,
1475-
this.color,
1476-
this.opacity,
1477-
this.colorBlendMode,
1478-
this.fit,
1479-
this.alignment = Alignment.center,
1480-
this.repeat = ImageRepeat.noRepeat,
1481-
this.centerSlice,
1482-
this.matchTextDirection = false,
1483-
this.gaplessPlayback = false,
1484-
this.filterQuality = FilterQuality.low,
1485-
this.isAntiAlias = false,
1486-
// `headers` skipped
1487-
this.cacheWidth,
1488-
this.cacheHeight,
1489-
});
1490-
1491-
final Uri src;
1492-
1493-
final double scale;
1494-
final ImageFrameBuilder? frameBuilder;
1495-
final ImageLoadingBuilder? loadingBuilder;
1496-
final ImageErrorWidgetBuilder? errorBuilder;
1497-
final String? semanticLabel;
1498-
final bool excludeFromSemantics;
1499-
final double? width;
1500-
final double? height;
1501-
final Color? color;
1502-
final Animation<double>? opacity;
1503-
final BlendMode? colorBlendMode;
1504-
final BoxFit? fit;
1505-
final AlignmentGeometry alignment;
1506-
final ImageRepeat repeat;
1507-
final Rect? centerSlice;
1508-
final bool matchTextDirection;
1509-
final bool gaplessPlayback;
1510-
final FilterQuality filterQuality;
1511-
final bool isAntiAlias;
1512-
// `headers` skipped
1513-
final int? cacheWidth;
1514-
final int? cacheHeight;
1515-
1516-
@override
1517-
Widget build(BuildContext context) {
1518-
final account = PerAccountStoreWidget.of(context).account;
1519-
1520-
return Image.network(
1521-
src.toString(),
1522-
1523-
scale: scale,
1524-
frameBuilder: frameBuilder,
1525-
loadingBuilder: loadingBuilder,
1526-
errorBuilder: errorBuilder,
1527-
semanticLabel: semanticLabel,
1528-
excludeFromSemantics: excludeFromSemantics,
1529-
width: width,
1530-
height: height,
1531-
color: color,
1532-
opacity: opacity,
1533-
colorBlendMode: colorBlendMode,
1534-
fit: fit,
1535-
alignment: alignment,
1536-
repeat: repeat,
1537-
centerSlice: centerSlice,
1538-
matchTextDirection: matchTextDirection,
1539-
gaplessPlayback: gaplessPlayback,
1540-
filterQuality: filterQuality,
1541-
isAntiAlias: isAntiAlias,
1542-
headers: {
1543-
// Only send the auth header to the server `auth` belongs to.
1544-
if (src.origin == account.realmUrl.origin) ...authHeader(
1545-
email: account.email, apiKey: account.apiKey,
1546-
),
1547-
...userAgentHeader(),
1548-
},
1549-
cacheWidth: cacheWidth,
1550-
cacheHeight: cacheHeight,
1551-
);
1552-
}
1553-
}
1554-
15551450
//
15561451
// Small helpers.
15571452
//

lib/widgets/emoji.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
33

44
import '../api/model/model.dart';
55
import '../model/emoji.dart';
6-
import 'content.dart';
6+
import 'image.dart';
77

88
/// A widget showing an emoji.
99
class EmojiWidget extends StatelessWidget {

lib/widgets/image.dart

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
import '../api/core.dart';
4+
import 'store.dart';
5+
6+
/// Like [Image.network], but includes [authHeader] if [src] is on-realm.
7+
///
8+
/// Use this to present image content in the ambient realm: avatars, images in
9+
/// messages, etc. Must have a [PerAccountStoreWidget] ancestor.
10+
///
11+
/// If [src] is an on-realm URL (it has the same origin as the ambient
12+
/// [Auth.realmUrl]), then an HTTP request to fetch the image will include the
13+
/// user's [authHeader].
14+
///
15+
/// If [src] is off-realm (e.g., a Gravatar URL), no auth header will be sent.
16+
///
17+
/// The image will be cached according to the cache behavior of [Image.network],
18+
/// which may mean the cache is shared between realms.
19+
class RealmContentNetworkImage extends StatelessWidget {
20+
const RealmContentNetworkImage(
21+
this.src, {
22+
super.key,
23+
this.scale = 1.0,
24+
this.frameBuilder,
25+
this.loadingBuilder,
26+
this.errorBuilder,
27+
this.semanticLabel,
28+
this.excludeFromSemantics = false,
29+
this.width,
30+
this.height,
31+
this.color,
32+
this.opacity,
33+
this.colorBlendMode,
34+
this.fit,
35+
this.alignment = Alignment.center,
36+
this.repeat = ImageRepeat.noRepeat,
37+
this.centerSlice,
38+
this.matchTextDirection = false,
39+
this.gaplessPlayback = false,
40+
this.filterQuality = FilterQuality.low,
41+
this.isAntiAlias = false,
42+
// `headers` skipped
43+
this.cacheWidth,
44+
this.cacheHeight,
45+
});
46+
47+
final Uri src;
48+
49+
final double scale;
50+
final ImageFrameBuilder? frameBuilder;
51+
final ImageLoadingBuilder? loadingBuilder;
52+
final ImageErrorWidgetBuilder? errorBuilder;
53+
final String? semanticLabel;
54+
final bool excludeFromSemantics;
55+
final double? width;
56+
final double? height;
57+
final Color? color;
58+
final Animation<double>? opacity;
59+
final BlendMode? colorBlendMode;
60+
final BoxFit? fit;
61+
final AlignmentGeometry alignment;
62+
final ImageRepeat repeat;
63+
final Rect? centerSlice;
64+
final bool matchTextDirection;
65+
final bool gaplessPlayback;
66+
final FilterQuality filterQuality;
67+
final bool isAntiAlias;
68+
// `headers` skipped
69+
final int? cacheWidth;
70+
final int? cacheHeight;
71+
72+
@override
73+
Widget build(BuildContext context) {
74+
final account = PerAccountStoreWidget.of(context).account;
75+
76+
return Image.network(
77+
src.toString(),
78+
79+
scale: scale,
80+
frameBuilder: frameBuilder,
81+
loadingBuilder: loadingBuilder,
82+
errorBuilder: errorBuilder,
83+
semanticLabel: semanticLabel,
84+
excludeFromSemantics: excludeFromSemantics,
85+
width: width,
86+
height: height,
87+
color: color,
88+
opacity: opacity,
89+
colorBlendMode: colorBlendMode,
90+
fit: fit,
91+
alignment: alignment,
92+
repeat: repeat,
93+
centerSlice: centerSlice,
94+
matchTextDirection: matchTextDirection,
95+
gaplessPlayback: gaplessPlayback,
96+
filterQuality: filterQuality,
97+
isAntiAlias: isAntiAlias,
98+
headers: {
99+
// Only send the auth header to the server `auth` belongs to.
100+
if (src.origin == account.realmUrl.origin) ...authHeader(
101+
email: account.email, apiKey: account.apiKey,
102+
),
103+
...userAgentHeader(),
104+
},
105+
cacheWidth: cacheWidth,
106+
cacheHeight: cacheHeight,
107+
);
108+
}
109+
}

lib/widgets/lightbox.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '../model/binding.dart';
1111
import 'actions.dart';
1212
import 'content.dart';
1313
import 'dialog.dart';
14+
import 'image.dart';
1415
import 'message_list.dart';
1516
import 'page.dart';
1617
import 'store.dart';

lib/widgets/user.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import '../api/model/model.dart';
44
import '../model/avatar_url.dart';
55
import '../model/binding.dart';
66
import '../model/presence.dart';
7-
import 'content.dart';
87
import 'emoji.dart';
98
import 'icons.dart';
9+
import 'image.dart';
1010
import 'store.dart';
1111
import 'theme.dart';
1212

test/widgets/checks.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import 'package:zulip/widgets/all_channels.dart';
99
import 'package:zulip/widgets/button.dart';
1010
import 'package:zulip/widgets/channel_colors.dart';
1111
import 'package:zulip/widgets/compose_box.dart';
12-
import 'package:zulip/widgets/content.dart';
1312
import 'package:zulip/widgets/emoji.dart';
1413
import 'package:zulip/widgets/emoji_reaction.dart';
14+
import 'package:zulip/widgets/image.dart';
1515
import 'package:zulip/widgets/login.dart';
1616
import 'package:zulip/widgets/message_list.dart';
1717
import 'package:zulip/widgets/page.dart';

test/widgets/content_test.dart

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import 'package:flutter/rendering.dart';
66
import 'package:flutter_checks/flutter_checks.dart';
77
import 'package:flutter_test/flutter_test.dart';
88
import 'package:url_launcher/url_launcher.dart';
9-
import 'package:zulip/api/core.dart';
109
import 'package:zulip/api/model/initial_snapshot.dart';
1110
import 'package:zulip/api/model/model.dart';
1211
import 'package:zulip/api/route/messages.dart';
@@ -16,10 +15,10 @@ import 'package:zulip/model/settings.dart';
1615
import 'package:zulip/model/store.dart';
1716
import 'package:zulip/widgets/content.dart';
1817
import 'package:zulip/widgets/icons.dart';
18+
import 'package:zulip/widgets/image.dart';
1919
import 'package:zulip/widgets/katex.dart';
2020
import 'package:zulip/widgets/message_list.dart';
2121
import 'package:zulip/widgets/page.dart';
22-
import 'package:zulip/widgets/store.dart';
2322
import 'package:zulip/widgets/text.dart';
2423

2524
import '../api/fake_api.dart';
@@ -1320,46 +1319,6 @@ void main() {
13201319
});
13211320
});
13221321

1323-
group('RealmContentNetworkImage', () {
1324-
final authHeaders = authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey);
1325-
1326-
Future<Map<String, List<String>>> actualHeaders(WidgetTester tester, Uri src) async {
1327-
addTearDown(testBinding.reset);
1328-
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
1329-
1330-
final httpClient = prepareBoringImageHttpClient();
1331-
1332-
await tester.pumpWidget(GlobalStoreWidget(
1333-
child: PerAccountStoreWidget(accountId: eg.selfAccount.id,
1334-
child: RealmContentNetworkImage(src))));
1335-
await tester.pump();
1336-
await tester.pump();
1337-
1338-
return httpClient.request.headers.values;
1339-
}
1340-
1341-
testWidgets('includes auth header if `src` on-realm', (tester) async {
1342-
check(await actualHeaders(tester, Uri.parse('https://chat.example/image.png')))
1343-
.deepEquals({
1344-
'Authorization': [authHeaders['Authorization']!],
1345-
'User-Agent': [userAgentHeader()['User-Agent']!],
1346-
});
1347-
debugNetworkImageHttpClientProvider = null;
1348-
});
1349-
1350-
testWidgets('excludes auth header if `src` off-realm', (tester) async {
1351-
check(await actualHeaders(tester, Uri.parse('https://other.example/image.png')))
1352-
.deepEquals({'User-Agent': [userAgentHeader()['User-Agent']!]});
1353-
debugNetworkImageHttpClientProvider = null;
1354-
});
1355-
1356-
testWidgets('throws if no `PerAccountStoreWidget` ancestor', (tester) async {
1357-
await tester.pumpWidget(
1358-
RealmContentNetworkImage(Uri.parse('https://zulip.invalid/path/to/image.png'), filterQuality: FilterQuality.medium));
1359-
check(tester.takeException()).isA<AssertionError>();
1360-
});
1361-
});
1362-
13631322
group('MessageTable', () {
13641323
testFontWeight('bold column header label',
13651324
// | a | b | c | d |

test/widgets/image_test.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter/widgets.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:zulip/api/core.dart';
5+
import 'package:zulip/widgets/image.dart';
6+
import 'package:zulip/widgets/store.dart';
7+
8+
import '../example_data.dart' as eg;
9+
import '../model/binding.dart';
10+
import '../test_images.dart';
11+
12+
void main() {
13+
TestZulipBinding.ensureInitialized();
14+
15+
group('RealmContentNetworkImage', () {
16+
final authHeaders = authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey);
17+
18+
Future<Map<String, List<String>>> actualHeaders(WidgetTester tester, Uri src) async {
19+
addTearDown(testBinding.reset);
20+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
21+
22+
final httpClient = prepareBoringImageHttpClient();
23+
24+
await tester.pumpWidget(GlobalStoreWidget(
25+
child: PerAccountStoreWidget(accountId: eg.selfAccount.id,
26+
child: RealmContentNetworkImage(src))));
27+
await tester.pump();
28+
await tester.pump();
29+
30+
return httpClient.request.headers.values;
31+
}
32+
33+
testWidgets('includes auth header if `src` on-realm', (tester) async {
34+
check(await actualHeaders(tester, Uri.parse('https://chat.example/image.png')))
35+
.deepEquals({
36+
'Authorization': [authHeaders['Authorization']!],
37+
'User-Agent': [userAgentHeader()['User-Agent']!],
38+
});
39+
debugNetworkImageHttpClientProvider = null;
40+
});
41+
42+
testWidgets('excludes auth header if `src` off-realm', (tester) async {
43+
check(await actualHeaders(tester, Uri.parse('https://other.example/image.png')))
44+
.deepEquals({'User-Agent': [userAgentHeader()['User-Agent']!]});
45+
debugNetworkImageHttpClientProvider = null;
46+
});
47+
48+
testWidgets('throws if no `PerAccountStoreWidget` ancestor', (tester) async {
49+
await tester.pumpWidget(
50+
RealmContentNetworkImage(Uri.parse('https://zulip.invalid/path/to/image.png'), filterQuality: FilterQuality.medium));
51+
check(tester.takeException()).isA<AssertionError>();
52+
});
53+
});
54+
}

0 commit comments

Comments
 (0)