import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../matrix/media.dart'; import '../state.dart'; import '../theme.dart'; /// A round avatar: the picture at [mxc] when there is one, else the first /// letter of [name] on a colour picked from [seed] (the user or room ID), so /// the same person always gets the same colour. class Avatar extends ConsumerStatefulWidget { const Avatar({ super.key, required this.name, required this.seed, this.mxc, this.size = 40, }); final String name; final String seed; final Uri? mxc; final double size; @override ConsumerState createState() => _AvatarState(); } class _AvatarState extends ConsumerState { Future? _bytes; @override void initState() { super.initState(); _load(); } @override void didUpdateWidget(Avatar old) { super.didUpdateWidget(old); if (old.mxc != widget.mxc || old.size != widget.size) _load(); } void _load() { final mxc = widget.mxc; _bytes = mxc == null || mxc.scheme != 'mxc' ? null : Media.thumbnail(ref.read(clientProvider), mxc, widget.size.round()); } static const _palette = [ Color(0xFF00A389), Color(0xFF1D5FD1), Color(0xFF8A5300), Color(0xFFB42318), Color(0xFF6D4AC2), Color(0xFF00806A), Color(0xFFC2410C), Color(0xFF0E7490), ]; @override Widget build(BuildContext context) { final size = widget.size; final hash = widget.seed.codeUnits.fold( 0, (h, c) => (h * 31 + c) & 0x7fffffff, ); final colour = _palette[hash.abs() % _palette.length]; final letter = _initial(widget.name); final fallback = Container( width: size, height: size, alignment: Alignment.center, decoration: BoxDecoration(color: colour, shape: BoxShape.circle), child: Text( letter, style: TextStyle( fontFamily: fontDisplay, fontWeight: FontWeight.w600, fontSize: size * 0.42, color: Colors.white, height: 1, ), ), ); final bytes = _bytes; if (bytes == null) return fallback; return FutureBuilder( future: bytes, builder: (context, snapshot) { final data = snapshot.data; if (data == null) return fallback; return ClipOval( child: Image.memory( data, width: size, height: size, fit: BoxFit.cover, gaplessPlayback: true, errorBuilder: (_, _, _) => fallback, ), ); }, ); } static String _initial(String name) { final trimmed = name.replaceFirst(RegExp(r'^[@#!+]'), '').trim(); if (trimmed.isEmpty) return '?'; return String.fromCharCode(trimmed.runes.first).toUpperCase(); } }