Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Banner #527

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ android {
}

kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you revert this? I also had some issues with this, will try to fix this in a version bump

}

sourceSets {
Expand Down
4 changes: 3 additions & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
Comment on lines +3 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
3 changes: 3 additions & 0 deletions lib/api/api_repository.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:reaxit/config.dart';
import 'package:reaxit/models.dart';
import 'package:reaxit/models/announcement.dart';
import 'package:reaxit/models/thabliod.dart';
import 'package:reaxit/api/exceptions.dart';

Expand Down Expand Up @@ -301,6 +302,8 @@ abstract class ApiRepository {
int? offset,
});

Future<List<Announcement>> getAnnouncements();

/// Get a list of [FrontpageArticle]s.
///
/// Use `limit` and `offset` for pagination. [ListResponse.count] is the
Expand Down
25 changes: 25 additions & 0 deletions lib/api/concrexit_api_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:reaxit/api/exceptions.dart';
import 'package:reaxit/config.dart';
import 'package:reaxit/models.dart';
import 'package:reaxit/models/thabliod.dart';
import 'package:reaxit/models/announcement.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

class LoggingClient extends oauth2.Client {
Expand Down Expand Up @@ -116,6 +117,10 @@ class ConcrexitApiRepository implements ApiRepository {
static Map<String, dynamic> _jsonDecode(Response response) =>
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;

/// Wrapper that utf-8 decodes the body of a response to json.
static List<dynamic> _jsonDecodeList(Response response) =>
jsonDecode(utf8.decode(response.bodyBytes)) as List<dynamic>;

/// A wrapper for requests that throws only [ApiException]s.
///
/// Translates exceptions that can be thrown by [oauth2.Client.send()],
Expand Down Expand Up @@ -1128,6 +1133,26 @@ class ConcrexitApiRepository implements ApiRepository {
);
}

@override
Future<List<Announcement>> getAnnouncements() async {
return sandbox(() async {
final uri = _uri(
path: '/announcements/announcements/',
);

final response = await _handleExceptions(() => _client.get(uri));
return await compute(_parseAnnouncements, response);
});
}

static List<Announcement> _parseAnnouncements(
Response response,
) {
return (_jsonDecodeList(response))
.map((json) => Announcement.fromJson(json as Map<String, dynamic>))
.toList();
}

@override
Future<Device> registerDevice({
required String token,
Expand Down
21 changes: 18 additions & 3 deletions lib/blocs/welcome_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:meta/meta.dart';
import 'package:reaxit/api/api_repository.dart';
import 'package:reaxit/api/exceptions.dart';
import 'package:reaxit/models.dart';
import 'package:reaxit/models/announcement.dart';

class WelcomeState extends Equatable {
/// This can only be null when [isLoading] or [hasException] is true.
Expand All @@ -16,6 +17,9 @@ class WelcomeState extends Equatable {
/// This can only be null when [isLoading] or [hasException] is true.
final List<BaseEvent>? upcomingEvents;

/// This can only be null when [isLoading] or [hasException] is true.
final List<Announcement>? announcements;

/// A message describing why there are no results.
final String? message;

Expand All @@ -24,32 +28,38 @@ class WelcomeState extends Equatable {

bool get hasException => message != null;
bool get hasResults =>
slides != null && articles != null && upcomingEvents != null;
slides != null &&
articles != null &&
upcomingEvents != null &&
announcements != null;

@protected
const WelcomeState({
required this.slides,
required this.articles,
required this.upcomingEvents,
required this.announcements,
required this.isLoading,
required this.message,
});

@override
List<Object?> get props =>
[slides, articles, upcomingEvents, message, isLoading];
[slides, articles, upcomingEvents, announcements, message, isLoading];

WelcomeState copyWith({
List<Slide>? slides,
List<FrontpageArticle>? articles,
List<BaseEvent>? upcomingEvents,
List<Announcement>? announcements,
bool? isLoading,
String? message,
}) =>
WelcomeState(
slides: slides ?? this.slides,
articles: articles ?? this.articles,
upcomingEvents: upcomingEvents ?? this.upcomingEvents,
announcements: announcements ?? this.announcements,
isLoading: isLoading ?? this.isLoading,
message: message ?? this.message,
);
Expand All @@ -58,17 +68,20 @@ class WelcomeState extends Equatable {
required List<Slide> this.slides,
required List<FrontpageArticle> this.articles,
required List<BaseEvent> this.upcomingEvents,
required List<Announcement> this.announcements,
}) : message = null,
isLoading = false;

const WelcomeState.loading({this.slides, this.articles, this.upcomingEvents})
const WelcomeState.loading(
{this.slides, this.articles, this.upcomingEvents, this.announcements})
: message = null,
isLoading = true;

const WelcomeState.failure({required String this.message})
: slides = null,
articles = null,
upcomingEvents = null,
announcements = null,
isLoading = false;
}

Expand All @@ -92,6 +105,7 @@ class WelcomeCubit extends Cubit<WelcomeState> {
ordering: 'start',
limit: 3,
);
final announcementsResponse = await api.getAnnouncements();

List<BaseEvent> events = eventsResponse.results
.map<BaseEvent>((e) => e)
Expand All @@ -109,6 +123,7 @@ class WelcomeCubit extends Cubit<WelcomeState> {
slides: slides,
articles: articlesResponse.results,
upcomingEvents: events,
announcements: announcementsResponse,
));
} on ApiException catch (exception) {
emit(WelcomeState.failure(message: exception.message));
Expand Down
16 changes: 16 additions & 0 deletions lib/models/announcement.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:json_annotation/json_annotation.dart';

part 'announcement.g.dart';

@JsonSerializable(fieldRename: FieldRename.snake)
class Announcement {
final String content;
final bool closeable;
final String icon;
final int? id;

const Announcement(this.content, this.closeable, this.icon, this.id);

factory Announcement.fromJson(Map<String, dynamic> json) =>
_$AnnouncementFromJson(json);
}
22 changes: 22 additions & 0 deletions lib/models/announcement.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions lib/routes.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:reaxit/config.dart' as config;
import 'package:reaxit/models.dart';
Expand All @@ -9,6 +10,7 @@ import 'package:reaxit/ui/screens.dart';
import 'package:reaxit/ui/screens/liked_photos_screen.dart';
import 'package:reaxit/ui/screens/thabloids_screen.dart';
import 'package:reaxit/ui/widgets.dart';
import 'package:reaxit/blocs.dart';

/// Returns true if [uri] is a deep link that can be handled by the app.
bool isDeepLink(Uri uri) {
Expand Down Expand Up @@ -39,6 +41,7 @@ final List<RegExp> _deepLinkRegExps = <RegExp>[
RegExp('^/association/societies(/[0-9]+)?/?\$'),
RegExp('^/association/committees(/[0-9]+)?/?\$'),
RegExp('^/association/boards/([0-9]{4}-[0-9]{4})/?\$'),
RegExp('^/user/edit-profile/')
];

final List<RouteBase> routes = [
Expand Down Expand Up @@ -498,4 +501,15 @@ final List<RouteBase> routes = [
name: 'pay',
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: PayScreen())),
GoRoute(
path: '/user/edit-profile',
name: 'profile',
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
child: ProfileScreen(
pk: BlocProvider.of<FullMemberCubit>(context).state.result!.pk,
member: state.extra as ListMember?,
),
),
),
];
97 changes: 97 additions & 0 deletions lib/ui/screens/welcome_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:reaxit/api/api_repository.dart';
import 'package:reaxit/blocs.dart';
import 'package:reaxit/models.dart';
import 'package:reaxit/routes.dart';
import 'package:reaxit/ui/widgets.dart';
import 'package:collection/collection.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:reaxit/models/announcement.dart';

class WelcomeScreen extends StatefulWidget {
@override
Expand All @@ -30,6 +32,15 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
);
}

Widget _makeAnnouncements(List<Announcement> announcements) {
return AnimatedSize(
curve: Curves.ease,
duration: const Duration(milliseconds: 300),
child: announcements.isNotEmpty
? Announcements(announcements)
: const SizedBox.shrink());
}

Widget _makeSlides(List<Slide> slides) {
return AnimatedSize(
curve: Curves.ease,
Expand Down Expand Up @@ -179,6 +190,8 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
key: const PageStorageKey('welcome'),
physics: const AlwaysScrollableScrollPhysics(),
children: [
_makeAnnouncements(state.announcements!),
if (state.announcements!.isNotEmpty) const Divider(height: 0),
_makeSlides(state.slides!),
if (state.slides!.isNotEmpty) const Divider(height: 0),
_makeArticles(state.articles!),
Expand Down Expand Up @@ -273,3 +286,87 @@ class _SlidesCarouselState extends State<SlidesCarousel> {
);
}
}

class Announcements extends StatefulWidget {
final List<Announcement> announcements;

const Announcements(this.announcements);

@override
State<Announcements> createState() => _AnnouncementState();
}

class _AnnouncementState extends State<Announcements> {
Widget _makeAnnouncement(Announcement announcement) {
return Container(
color: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.all(20),
child: Row(
children: [
const Padding(
padding: EdgeInsets.fromLTRB(0, 0, 20, 0),
child: Icon(Icons.campaign),
),
Expanded(
child: HtmlWidget(
announcement.content,
customStylesBuilder: (element) {
if (element.localName == 'a') {
return {
'color': 'white', // Change link color to red
'text-decoration': 'none', // Remove default underline
'font-weight': 'bold',
'border-bottom': '2px solid white',
};
}
return null;
},
onTapUrl: (String url) async {
Uri uri = Uri(path: url);
String host =
RepositoryProvider.of<ApiRepository>(context).config.host;
if (uri.scheme.isEmpty) uri = uri.replace(scheme: 'https');
if (uri.host.isEmpty) uri = uri.replace(host: host);
if (isDeepLink(uri)) {
context.push(Uri(
path: uri.path,
query: uri.query,
).toString());
return true;
} else {
final messenger = ScaffoldMessenger.of(context);
try {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (_) {
messenger.showSnackBar(SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Could not open "$url".'),
));
}
return true;
}
},
),
),
if (announcement.closeable)
CloseButton(
onPressed: () =>
setState(() => widget.announcements.remove(announcement)))
],
),
);
}

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final announcement in widget.announcements) ...[
//const Divider(height: 8),
_makeAnnouncement(announcement),
],
],
);
}
}
Loading
Loading