@@ -12,9 +12,11 @@ import '../api/core.dart';
1212import '../api/model/model.dart' ;
1313import '../generated/l10n/zulip_localizations.dart' ;
1414import '../model/avatar_url.dart' ;
15+ import '../model/binding.dart' ;
1516import '../model/content.dart' ;
1617import '../model/internal_link.dart' ;
1718import '../model/katex.dart' ;
19+ import '../model/presence.dart' ;
1820import 'actions.dart' ;
1921import 'code_block.dart' ;
2022import 'dialog.dart' ;
@@ -1662,17 +1664,23 @@ class Avatar extends StatelessWidget {
16621664 required this .userId,
16631665 required this .size,
16641666 required this .borderRadius,
1667+ this .backgroundColor,
1668+ this .omitPresenceStatus = false ,
16651669 });
16661670
16671671 final int userId;
16681672 final double size;
16691673 final double borderRadius;
1674+ final Color ? backgroundColor;
1675+ final bool omitPresenceStatus;
16701676
16711677 @override
16721678 Widget build (BuildContext context) {
16731679 return AvatarShape (
16741680 size: size,
16751681 borderRadius: borderRadius,
1682+ backgroundColor: backgroundColor,
1683+ userIdForPresence: omitPresenceStatus ? null : userId,
16761684 child: AvatarImage (userId: userId, size: size));
16771685 }
16781686}
@@ -1722,26 +1730,163 @@ class AvatarImage extends StatelessWidget {
17221730}
17231731
17241732/// A rounded square shape, to wrap an [AvatarImage] or similar.
1733+ ///
1734+ /// If [userIdForPresence] is provided, this will paint a [PresenceCircle]
1735+ /// on the shape.
17251736class AvatarShape extends StatelessWidget {
17261737 const AvatarShape ({
17271738 super .key,
17281739 required this .size,
17291740 required this .borderRadius,
1741+ this .backgroundColor,
1742+ this .userIdForPresence,
17301743 required this .child,
17311744 });
17321745
17331746 final double size;
17341747 final double borderRadius;
1748+ final Color ? backgroundColor;
1749+ final int ? userIdForPresence;
17351750 final Widget child;
17361751
17371752 @override
17381753 Widget build (BuildContext context) {
1754+ Widget result = child;
1755+
1756+ if (userIdForPresence != null ) {
1757+ final presenceCircleSize = size / 4 ; // TODO(design) is this right?
1758+ result = Stack (children: [
1759+ result,
1760+ Positioned .directional (textDirection: Directionality .of (context),
1761+ end: 0 ,
1762+ bottom: 0 ,
1763+ child: PresenceCircle (
1764+ userId: userIdForPresence! ,
1765+ size: presenceCircleSize,
1766+ backgroundColor: backgroundColor)),
1767+ ]);
1768+ }
1769+
17391770 return SizedBox .square (
17401771 dimension: size,
17411772 child: ClipRRect (
17421773 borderRadius: BorderRadius .all (Radius .circular (borderRadius)),
17431774 clipBehavior: Clip .antiAlias,
1744- child: child));
1775+ child: result));
1776+ }
1777+ }
1778+
1779+ /// The green or orange-gradient circle representing [PresenceStatus] .
1780+ ///
1781+ /// [backgroundColor] is required and must not be [Colors.transparent] .
1782+ /// It exists to match the background on which the avatar image is painted.
1783+ ///
1784+ /// By default, nothing paints for a user in the "offline" status
1785+ /// (i.e. a user without a [PresenceStatus] ).
1786+ /// Pass true for [explicitOffline] to paint a gray circle.
1787+ class PresenceCircle extends StatefulWidget {
1788+ const PresenceCircle ({
1789+ super .key,
1790+ required this .userId,
1791+ required this .size,
1792+ this .backgroundColor,
1793+ this .explicitOffline = false ,
1794+ });
1795+
1796+ final int userId;
1797+ final double size;
1798+ final Color ? backgroundColor;
1799+ final bool explicitOffline;
1800+
1801+ /// Creates a [WidgetSpan] with a [PresenceCircle] , for use in rich text
1802+ /// before a user's name.
1803+ static InlineSpan asWidgetSpan ({
1804+ required int userId,
1805+ required double fontSize,
1806+ required TextScaler textScaler,
1807+ Color ? backgroundColor,
1808+ }) {
1809+ final size = textScaler.scale (fontSize) / 2 ;
1810+ return WidgetSpan (
1811+ alignment: PlaceholderAlignment .middle,
1812+ child: Padding (
1813+ padding: const EdgeInsetsDirectional .only (end: 4 ),
1814+ child: PresenceCircle (
1815+ userId: userId,
1816+ size: size,
1817+ backgroundColor: backgroundColor,
1818+ explicitOffline: true )));
1819+ }
1820+
1821+ @override
1822+ State <PresenceCircle > createState () => _PresenceCircleState ();
1823+ }
1824+
1825+ class _PresenceCircleState extends State <PresenceCircle > with PerAccountStoreAwareStateMixin {
1826+ Presence ? model;
1827+
1828+ @override
1829+ void onNewStore () {
1830+ model? .removeListener (_modelChanged);
1831+ model = PerAccountStoreWidget .of (context).presence
1832+ ..addListener (_modelChanged);
1833+ }
1834+
1835+ @override
1836+ void dispose () {
1837+ model! .removeListener (_modelChanged);
1838+ super .dispose ();
1839+ }
1840+
1841+ void _modelChanged () {
1842+ setState (() {
1843+ // The actual state lives in [model].
1844+ // This method was called because that just changed.
1845+ });
1846+ }
1847+
1848+ @override
1849+ Widget build (BuildContext context) {
1850+ final status = model! .presenceStatusForUser (
1851+ widget.userId, utcNow: ZulipBinding .instance.utcNow ());
1852+ final designVariables = DesignVariables .of (context);
1853+ final effectiveBackgroundColor = widget.backgroundColor ?? designVariables.mainBackground;
1854+ assert (effectiveBackgroundColor != Colors .transparent);
1855+
1856+ Color ? color;
1857+ LinearGradient ? gradient;
1858+
1859+ switch (status) {
1860+ case null :
1861+ if (widget.explicitOffline) {
1862+ // TODO(a11y) this should be an open circle, like on web,
1863+ // to differentiate by shape (vs. the "active" status which is also
1864+ // a solid circle)
1865+ color = designVariables.statusAway;
1866+ } else {
1867+ return SizedBox .square (dimension: widget.size);
1868+ }
1869+ case PresenceStatus .active:
1870+ color = designVariables.statusOnline;
1871+ case PresenceStatus .idle:
1872+ gradient = LinearGradient (
1873+ begin: AlignmentDirectional .centerStart,
1874+ end: AlignmentDirectional .centerEnd,
1875+ colors: [designVariables.statusIdle, effectiveBackgroundColor],
1876+ stops: [0.05 , 1.00 ],
1877+ );
1878+ }
1879+
1880+ return SizedBox .square (dimension: widget.size,
1881+ child: DecoratedBox (
1882+ decoration: BoxDecoration (
1883+ border: Border .all (
1884+ color: effectiveBackgroundColor,
1885+ width: 2 ,
1886+ strokeAlign: BorderSide .strokeAlignOutside),
1887+ color: color,
1888+ gradient: gradient,
1889+ shape: BoxShape .circle)));
17451890 }
17461891}
17471892
0 commit comments