diff --git a/wien_talks/wien_talks_client/lib/src/protocol/client.dart b/wien_talks/wien_talks_client/lib/src/protocol/client.dart index 3d49693..a580f14 100644 --- a/wien_talks/wien_talks_client/lib/src/protocol/client.dart +++ b/wien_talks/wien_talks_client/lib/src/protocol/client.dart @@ -23,21 +23,6 @@ class EndpointQuote extends _i1.EndpointRef { @override String get name => 'quote'; - _i2.Future updateQuote(_i3.Quote quote) => - caller.callServerEndpoint( - 'quote', - 'updateQuote', - {'quote': quote}, - ); - - _i2.Stream<_i3.Quote> quoteUpdates() => - caller.callStreamingServerEndpoint<_i2.Stream<_i3.Quote>, _i3.Quote>( - 'quote', - 'quoteUpdates', - {}, - {}, - ); - _i2.Future<_i3.Quote> createQuote(_i4.CreateQuoteRequest req) => caller.callServerEndpoint<_i3.Quote>( 'quote', @@ -45,18 +30,18 @@ class EndpointQuote extends _i1.EndpointRef { {'req': req}, ); - _i2.Future<_i3.Quote> getQuoteById(int id) => - caller.callServerEndpoint<_i3.Quote>( + _i2.Future updateQuote(_i3.Quote quote) => + caller.callServerEndpoint( 'quote', - 'getQuoteById', - {'id': id}, + 'updateQuote', + {'quote': quote}, ); - _i2.Future> getAllQuotes() => + _i2.Future> getAllQuotes({required int limit}) => caller.callServerEndpoint>( 'quote', 'getAllQuotes', - {}, + {'limit': limit}, ); } diff --git a/wien_talks/wien_talks_flutter/lib/main.dart b/wien_talks/wien_talks_flutter/lib/main.dart index e0494e0..0ced7fe 100644 --- a/wien_talks/wien_talks_flutter/lib/main.dart +++ b/wien_talks/wien_talks_flutter/lib/main.dart @@ -3,7 +3,7 @@ import 'package:wien_talks_flutter/helper/funmap_mgr.dart'; import 'package:wien_talks_flutter/helper/go_router.dart'; void main() { - FunmapMgr(); + FunmapMgr().configure(); runApp(const MyApp()); } diff --git a/wien_talks/wien_talks_flutter/lib/news_screen.dart b/wien_talks/wien_talks_flutter/lib/news_screen.dart index 0463223..84507cb 100644 --- a/wien_talks/wien_talks_flutter/lib/news_screen.dart +++ b/wien_talks/wien_talks_flutter/lib/news_screen.dart @@ -17,7 +17,7 @@ class NewsScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ HeadingText(text: "Latest news"), - QuoteList(), + ShowLatestNewsWidget(), SizedBox( height: 30, ), diff --git a/wien_talks/wien_talks_flutter/lib/show_latest_news_widget.dart b/wien_talks/wien_talks_flutter/lib/show_latest_news_widget.dart index 0f50d2c..4dcf12b 100644 --- a/wien_talks/wien_talks_flutter/lib/show_latest_news_widget.dart +++ b/wien_talks/wien_talks_flutter/lib/show_latest_news_widget.dart @@ -1,27 +1,87 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:wien_talks_flutter/widgets/heading_text.dart'; +import 'package:wien_talks_client/wien_talks_client.dart'; +import 'package:wien_talks_flutter/helper/funmap_mgr.dart'; -import 'helper/funmap_mgr.dart'; - -class ShowLatestNewsWidget extends StatelessWidget { +class ShowLatestNewsWidget extends StatefulWidget { const ShowLatestNewsWidget({super.key}); + @override + State createState() => _ShowLatestNewsWidgetState(); +} + +class _ShowLatestNewsWidgetState extends State { + final _controller = StreamController>.broadcast(); + Timer? _timer; + + @override + void initState() { + super.initState(); + _reload(); + _timer = Timer.periodic(const Duration(seconds: 30), (_) => _reload()); + } + + Future _reload() async { + try { + final quotes = await FunmapMgr().client.quote.getAllQuotes(limit: 200); + _controller.add(quotes); + } catch (e, st) { + _controller.addError(e, st); + } + } + + @override + void dispose() { + _timer?.cancel(); + _controller.close(); + super.dispose(); + } @override Widget build(BuildContext context) { - return StreamBuilder( - stream: FunmapMgr().client.quote.stream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - HeadingText(text: "Latest news"), - if (snapshot.hasError) Text('Error: ${snapshot.error}'), - Text(snapshot.data ?? "Be the first to submit amazing news!", - style: TextStyle( - fontSize: 20, - color: Theme.of(context).colorScheme.error)), - ], + return RefreshIndicator( + onRefresh: _reload, + child: StreamBuilder>( + stream: _controller.stream, + initialData: const [], + builder: (context, snap) { + if (snap.hasError) { + return ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text('Error: ${snap.error}'), + ), + ], + ); + } + final quotes = snap.data ?? const []; + if (quotes.isEmpty) { + return ListView( + children: const [ + Padding( + padding: EdgeInsets.all(16), + child: Text('No quotes yet. Pull to refresh.'), + ), + ], + ); + } + return ListView.separated( + itemCount: quotes.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, i) { + final q = quotes[i]; + return ListTile( + title: Text(q.text), + subtitle: Text([ + if ((q.authorName ?? '').isNotEmpty) q.authorName!, + q.createdAt.toLocal().toString(), + ].where((e) => e.isNotEmpty).join(' ยท ')), + ); + }, ); - }); + }, + ), + ); } } diff --git a/wien_talks/wien_talks_flutter/lib/widgets/add-quote-fab.dart b/wien_talks/wien_talks_flutter/lib/widgets/add_quote_fab.dart similarity index 100% rename from wien_talks/wien_talks_flutter/lib/widgets/add-quote-fab.dart rename to wien_talks/wien_talks_flutter/lib/widgets/add_quote_fab.dart diff --git a/wien_talks/wien_talks_server/lib/src/generated/endpoints.dart b/wien_talks/wien_talks_server/lib/src/generated/endpoints.dart index a9cb545..2054dea 100644 --- a/wien_talks/wien_talks_server/lib/src/generated/endpoints.dart +++ b/wien_talks/wien_talks_server/lib/src/generated/endpoints.dart @@ -11,9 +11,9 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:serverpod/serverpod.dart' as _i1; import '../quotes/quotes_endpoint.dart' as _i2; -import 'package:wien_talks_server/src/generated/quotes/quote.dart' as _i3; import 'package:wien_talks_server/src/generated/quotes/create_quote.dart' - as _i4; + as _i3; +import 'package:wien_talks_server/src/generated/quotes/quote.dart' as _i4; import 'package:serverpod_auth_server/serverpod_auth_server.dart' as _i5; class Endpoints extends _i1.EndpointDispatch { @@ -31,30 +31,12 @@ class Endpoints extends _i1.EndpointDispatch { name: 'quote', endpoint: endpoints['quote']!, methodConnectors: { - 'updateQuote': _i1.MethodConnector( - name: 'updateQuote', - params: { - 'quote': _i1.ParameterDescription( - name: 'quote', - type: _i1.getType<_i3.Quote>(), - nullable: false, - ) - }, - call: ( - _i1.Session session, - Map params, - ) async => - (endpoints['quote'] as _i2.QuoteEndpoint).updateQuote( - session, - params['quote'], - ), - ), 'createQuote': _i1.MethodConnector( name: 'createQuote', params: { 'req': _i1.ParameterDescription( name: 'req', - type: _i1.getType<_i4.CreateQuoteRequest>(), + type: _i1.getType<_i3.CreateQuoteRequest>(), nullable: false, ) }, @@ -67,11 +49,29 @@ class Endpoints extends _i1.EndpointDispatch { params['req'], ), ), - 'getQuoteById': _i1.MethodConnector( - name: 'getQuoteById', + 'updateQuote': _i1.MethodConnector( + name: 'updateQuote', params: { - 'id': _i1.ParameterDescription( - name: 'id', + 'quote': _i1.ParameterDescription( + name: 'quote', + type: _i1.getType<_i4.Quote>(), + nullable: false, + ) + }, + call: ( + _i1.Session session, + Map params, + ) async => + (endpoints['quote'] as _i2.QuoteEndpoint).updateQuote( + session, + params['quote'], + ), + ), + 'getAllQuotes': _i1.MethodConnector( + name: 'getAllQuotes', + params: { + 'limit': _i1.ParameterDescription( + name: 'limit', type: _i1.getType(), nullable: false, ) @@ -80,32 +80,11 @@ class Endpoints extends _i1.EndpointDispatch { _i1.Session session, Map params, ) async => - (endpoints['quote'] as _i2.QuoteEndpoint).getQuoteById( + (endpoints['quote'] as _i2.QuoteEndpoint).getAllQuotes( session, - params['id'], + limit: params['limit'], ), ), - 'getAllQuotes': _i1.MethodConnector( - name: 'getAllQuotes', - params: {}, - call: ( - _i1.Session session, - Map params, - ) async => - (endpoints['quote'] as _i2.QuoteEndpoint).getAllQuotes(session), - ), - 'quoteUpdates': _i1.MethodStreamConnector( - name: 'quoteUpdates', - params: {}, - streamParams: {}, - returnType: _i1.MethodStreamReturnType.streamType, - call: ( - _i1.Session session, - Map params, - Map streamParams, - ) => - (endpoints['quote'] as _i2.QuoteEndpoint).quoteUpdates(session), - ), }, ); modules['serverpod_auth'] = _i5.Endpoints()..initializeEndpoints(server); diff --git a/wien_talks/wien_talks_server/lib/src/generated/protocol.yaml b/wien_talks/wien_talks_server/lib/src/generated/protocol.yaml index fd8118a..c4c3b61 100644 --- a/wien_talks/wien_talks_server/lib/src/generated/protocol.yaml +++ b/wien_talks/wien_talks_server/lib/src/generated/protocol.yaml @@ -1,6 +1,4 @@ quote: - - updateQuote: - - quoteUpdates: - createQuote: - - getQuoteById: + - updateQuote: - getAllQuotes: diff --git a/wien_talks/wien_talks_server/lib/src/quotes/quotes_endpoint.dart b/wien_talks/wien_talks_server/lib/src/quotes/quotes_endpoint.dart index f57732e..2da2850 100644 --- a/wien_talks/wien_talks_server/lib/src/quotes/quotes_endpoint.dart +++ b/wien_talks/wien_talks_server/lib/src/quotes/quotes_endpoint.dart @@ -1,34 +1,22 @@ -import 'dart:math'; +// lib/src/endpoints/quote_endpoint.dart +import 'dart:async'; import 'package:serverpod/serverpod.dart'; import 'package:wien_talks_server/src/generated/protocol.dart'; import 'package:wien_talks_server/src/quotes/quote_util.dart'; -class QuoteEndpoint extends Endpoint { +class ShowLatestNewsWidget extends Endpoint { static const _channelQuoteUpdates = 'quote-updates'; - Future updateQuote(Session session, Quote quote) async { - await Quote.db.updateRow(session, quote); - await session.messages.postMessage(_channelQuoteUpdates, quote); - } - - Stream quoteUpdates(Session session) async* { - var updateStream = - session.messages.createStream(_channelQuoteUpdates); - - await for (var quote in updateStream) { - yield quote; - } - } - Future createQuote(Session session, CreateQuoteRequest req) async { final authInfo = await session.authenticated; - final userId = Random().nextInt(100); + final userId = authInfo?.userId; - String text = validateQuote(req); + final text = validateQuote(req); - final quote = Quote( - userId: userId, + final toInsert = Quote( + id: 0, + userId: userId ?? 12, text: text, authorName: req.authorName, lat: req.lat, @@ -39,36 +27,61 @@ class QuoteEndpoint extends Endpoint { downvotes: 0, ); - final inserted = await session.db.insertRow(quote); + final inserted = await session.db.insertRow(toInsert); await session.messages.postMessage(_channelQuoteUpdates, inserted); - return inserted; } - Future getQuoteById(Session session, int id) async { - final quote = await Quote.db.findById(session, id); - if (quote != null) { - return quote; - } - - throw Exception('Quote not found'); + Future updateQuote(Session session, Quote quote) async { + await Quote.db.updateRow(session, quote); + await session.messages.postMessage(_channelQuoteUpdates, quote); } - Future> getAllQuotes(Session session) async { - final quotes = await Quote.db.find(session); - return quotes; + Future> getAllQuotes(Session session, {int limit = 200}) async { + final quoteList = await Quote.db.find(session); + return quoteList; } - Stream streamAllQuotes( - StreamingSession session, { - int limit = 200, - }) async* { + Future> streamAllQuotes(StreamingSession session, + {int limit = 200}) async { if (limit <= 0 || limit > 500) limit = 200; - final quoteStream = session.messages.createStream('quotes'); + final controller = StreamController(); + final live = session.messages.createStream(_channelQuoteUpdates); + final liveSub = live.listen( + (q) { + if (q.visibility == 0) controller.add(q); + }, + onError: controller.addError, + onDone: () { + if (!controller.isClosed) controller.close(); + }, + cancelOnError: false, + ); - await for (final Quote quote in quoteStream) { - yield quote; - } + () async* { + try { + final snapshot = await Quote.db.find( + session, + where: (t) => t.visibility.equals(0), + orderBy: (t) => t.createdAt, + orderDescending: true, + limit: limit, + ); + + for (final q in snapshot.reversed) { + controller.add(q); + } + } catch (e, st) { + controller.addError(e, st); + } + }(); + + await session.close().then((_) async { + await liveSub.cancel(); + await controller.close(); + }); + + return controller.stream; } } diff --git a/wien_talks/wien_talks_server/test/integration/test_tools/serverpod_test_tools.dart b/wien_talks/wien_talks_server/test/integration/test_tools/serverpod_test_tools.dart index aa1b0e9..87bbd13 100644 --- a/wien_talks/wien_talks_server/test/integration/test_tools/serverpod_test_tools.dart +++ b/wien_talks/wien_talks_server/test/integration/test_tools/serverpod_test_tools.dart @@ -130,64 +130,6 @@ class _QuoteEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future updateQuote( - _i1.TestSessionBuilder sessionBuilder, - _i4.Quote quote, - ) async { - return _i1.callAwaitableFunctionAndHandleExceptions(() async { - var _localUniqueSession = - (sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild( - endpoint: 'quote', - method: 'updateQuote', - ); - try { - var _localCallContext = await _endpointDispatch.getMethodCallContext( - createSessionCallback: (_) => _localUniqueSession, - endpointPath: 'quote', - methodName: 'updateQuote', - parameters: _i1.testObjectToJson({'quote': quote}), - serializationManager: _serializationManager, - ); - var _localReturnValue = await (_localCallContext.method.call( - _localUniqueSession, - _localCallContext.arguments, - ) as _i3.Future); - return _localReturnValue; - } finally { - await _localUniqueSession.close(); - } - }); - } - - _i3.Stream<_i4.Quote> quoteUpdates(_i1.TestSessionBuilder sessionBuilder) { - var _localTestStreamManager = _i1.TestStreamManager<_i4.Quote>(); - _i1.callStreamFunctionAndHandleExceptions( - () async { - var _localUniqueSession = - (sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild( - endpoint: 'quote', - method: 'quoteUpdates', - ); - var _localCallContext = - await _endpointDispatch.getMethodStreamCallContext( - createSessionCallback: (_) => _localUniqueSession, - endpointPath: 'quote', - methodName: 'quoteUpdates', - arguments: {}, - requestedInputStreams: [], - serializationManager: _serializationManager, - ); - await _localTestStreamManager.callStreamMethod( - _localCallContext, - _localUniqueSession, - {}, - ); - }, - _localTestStreamManager.outputStreamController, - ); - return _localTestStreamManager.outputStreamController.stream; - } - _i3.Future<_i4.Quote> createQuote( _i1.TestSessionBuilder sessionBuilder, _i5.CreateQuoteRequest req, @@ -217,28 +159,28 @@ class _QuoteEndpoint { }); } - _i3.Future<_i4.Quote> getQuoteById( + _i3.Future updateQuote( _i1.TestSessionBuilder sessionBuilder, - int id, + _i4.Quote quote, ) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = (sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild( endpoint: 'quote', - method: 'getQuoteById', + method: 'updateQuote', ); try { var _localCallContext = await _endpointDispatch.getMethodCallContext( createSessionCallback: (_) => _localUniqueSession, endpointPath: 'quote', - methodName: 'getQuoteById', - parameters: _i1.testObjectToJson({'id': id}), + methodName: 'updateQuote', + parameters: _i1.testObjectToJson({'quote': quote}), serializationManager: _serializationManager, ); var _localReturnValue = await (_localCallContext.method.call( _localUniqueSession, _localCallContext.arguments, - ) as _i3.Future<_i4.Quote>); + ) as _i3.Future); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -247,7 +189,9 @@ class _QuoteEndpoint { } _i3.Future> getAllQuotes( - _i1.TestSessionBuilder sessionBuilder) async { + _i1.TestSessionBuilder sessionBuilder, { + required int limit, + }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = (sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild( @@ -259,7 +203,7 @@ class _QuoteEndpoint { createSessionCallback: (_) => _localUniqueSession, endpointPath: 'quote', methodName: 'getAllQuotes', - parameters: _i1.testObjectToJson({}), + parameters: _i1.testObjectToJson({'limit': limit}), serializationManager: _serializationManager, ); var _localReturnValue = await (_localCallContext.method.call(