implemented chat long-polling and optimistic updates, centralized notification management, optimized avatar caching

This commit is contained in:
2026-05-10 15:47:55 +02:00
parent 6ae396e605
commit 1458d8ce49
15 changed files with 712 additions and 146 deletions
@@ -37,40 +37,10 @@ class AppointmentTile extends StatelessWidget {
borderRadius: _radius,
color: color,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
_AdaptiveTitle(
text: appointment.subject,
fontSize: kAppointmentTitleFontSize,
minFontSize: kAppointmentTitleMinFontSize,
fontWeight: FontWeight.w500,
),
if (isCustom) ...[
if (description.isNotEmpty)
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 1),
child: _WrappingBody(
text: description,
fontSize: kAppointmentBodyFontSize,
lineHeight: kAppointmentBodyLineHeight,
),
),
),
] else ...[
for (final line
in description
.split('\n')
.where((p) => p.isNotEmpty)
.take(2))
_ScaledLine(
text: line,
fontSize: kAppointmentBodyFontSize,
),
],
],
child: _TileContent(
title: appointment.subject,
description: description,
isCustom: isCustom,
),
),
),
@@ -96,6 +66,91 @@ class AppointmentTile extends StatelessWidget {
}
}
/// Picks how many lines fit into the calendar slot's height. Title gets
/// first dibs; if not even one minimum-size title line fits, the column
/// collapses to keep the slot from overflowing.
class _TileContent extends StatelessWidget {
final String title;
final String description;
final bool isCustom;
const _TileContent({
required this.title,
required this.description,
required this.isCustom,
});
@override
Widget build(BuildContext context) {
final scaler = MediaQuery.textScalerOf(context);
final titleLineHeight = scaler.scale(kAppointmentTitleMinFontSize) * 1.1;
final bodyLineHeight = scaler.scale(kAppointmentBodyFontSize) * 1.1;
final titleWidget = _AdaptiveTitle(
text: title,
fontSize: kAppointmentTitleFontSize,
minFontSize: kAppointmentTitleMinFontSize,
fontWeight: FontWeight.w500,
);
return LayoutBuilder(
builder: (context, constraints) {
final available = constraints.maxHeight;
// Slot too short for even one min-size title line — drop text
// entirely; the coloured rectangle is enough.
if (available < titleLineHeight) return const SizedBox.shrink();
final remaining =
(available - titleLineHeight).clamp(0.0, double.infinity);
final bodyLineCapacity = (remaining / bodyLineHeight).floor();
if (isCustom) {
if (description.isEmpty || bodyLineCapacity <= 0) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [titleWidget],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
titleWidget,
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 1),
child: _WrappingBody(
text: description,
fontSize: kAppointmentBodyFontSize,
lineHeight: kAppointmentBodyLineHeight,
),
),
),
],
);
}
final maxBodyLines = bodyLineCapacity.clamp(0, 2);
final lines = description
.split('\n')
.where((p) => p.isNotEmpty)
.take(maxBodyLines)
.toList(growable: false);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
titleWidget,
for (final line in lines)
_ScaledLine(text: line, fontSize: kAppointmentBodyFontSize),
],
);
},
);
}
}
/// Renders the appointment title. Scales down to fit the available width via
/// [FittedBox], but never below [minFontSize] — when even the minimum size
/// overflows, the text is rendered at [minFontSize] with an ellipsis.