Merge branch 'flutter_app'
This commit is contained in:
commit
dac268f40b
2
flutter/.gitignore
vendored
2
flutter/.gitignore
vendored
@ -2,6 +2,8 @@
|
||||
|
||||
*.keystore
|
||||
|
||||
firepit-log.txt
|
||||
flutter_jank_*
|
||||
|
||||
|
||||
#######################################################################################################################
|
||||
|
@ -7,6 +7,9 @@ run:
|
||||
test:
|
||||
dart analyze
|
||||
|
||||
fix:
|
||||
dart fix --apply
|
||||
|
||||
gen:
|
||||
dart run build_runner build
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
|
||||
|
||||
pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' )"
|
||||
pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' | tail -n 1 )"
|
||||
|
||||
if [ -z "$pid" ]; then
|
||||
red "No [flutter run] process found - exiting"
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -7,7 +7,6 @@ import 'package:simplecloudnotifier/models/client.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
@ -182,6 +181,16 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<ChannelWithSubscription> getChannel(TokenSource auth, String cid) async {
|
||||
return await _request(
|
||||
name: 'getChannel',
|
||||
method: 'GET',
|
||||
relURL: 'users/${auth.getUserID()}/channels/${cid}',
|
||||
fn: ChannelWithSubscription.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<(String, List<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
|
||||
return await _request(
|
||||
name: 'getMessageList',
|
||||
|
@ -1,6 +1,6 @@
|
||||
class APIException implements Exception {
|
||||
final int httpStatus;
|
||||
final String error;
|
||||
final int error;
|
||||
final String errHighlight;
|
||||
final String message;
|
||||
|
||||
|
@ -11,46 +11,64 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
required this.showThemeSwitch,
|
||||
required this.showDebug,
|
||||
required this.showSearch,
|
||||
required this.showShare,
|
||||
this.onShare = null,
|
||||
}) : super(key: key);
|
||||
|
||||
final String? title;
|
||||
final bool showThemeSwitch;
|
||||
final bool showDebug;
|
||||
final bool showSearch;
|
||||
final bool showShare;
|
||||
final void Function()? onShare;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var actions = <Widget>[];
|
||||
|
||||
if (showThemeSwitch) {
|
||||
actions.add(Consumer<AppTheme>(
|
||||
builder: (context, appTheme, child) => IconButton(
|
||||
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
|
||||
tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode',
|
||||
onPressed: appTheme.switchDarkMode,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
}
|
||||
|
||||
if (showDebug) {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
||||
tooltip: 'Debug',
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute<DebugMainPage>(builder: (context) => DebugMainPage()));
|
||||
},
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
}
|
||||
|
||||
if (showSearch) {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
|
||||
tooltip: 'Search',
|
||||
onPressed: () {/*TODO*/},
|
||||
));
|
||||
} else if (showShare) {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidShareNodes),
|
||||
tooltip: 'Share',
|
||||
onPressed: onShare ?? () {},
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
}
|
||||
|
||||
return AppBar(
|
||||
title: Text(title ?? 'Simple Cloud Notifier 2.0'),
|
||||
actions: <Widget>[
|
||||
if (showThemeSwitch)
|
||||
Consumer<AppTheme>(
|
||||
builder: (context, appTheme, child) => IconButton(
|
||||
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
|
||||
tooltip: 'Debug',
|
||||
onPressed: () {
|
||||
appTheme.switchDarkMode();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!showThemeSwitch) SizedBox.square(dimension: 40),
|
||||
if (showDebug)
|
||||
IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
||||
tooltip: 'Debug',
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute<DebugMainPage>(builder: (context) => DebugMainPage()));
|
||||
},
|
||||
),
|
||||
if (!showDebug) SizedBox.square(dimension: 40),
|
||||
if (showSearch)
|
||||
IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
|
||||
tooltip: 'Search',
|
||||
onPressed: () {},
|
||||
),
|
||||
if (!showSearch) SizedBox.square(dimension: 40),
|
||||
],
|
||||
actions: actions,
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ class SCNScaffold extends StatelessWidget {
|
||||
this.showThemeSwitch = true,
|
||||
this.showDebug = true,
|
||||
this.showSearch = true,
|
||||
this.showShare = false,
|
||||
this.onShare = null,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
@ -16,6 +18,8 @@ class SCNScaffold extends StatelessWidget {
|
||||
final bool showThemeSwitch;
|
||||
final bool showDebug;
|
||||
final bool showSearch;
|
||||
final bool showShare;
|
||||
final void Function()? onShare;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -25,6 +29,8 @@ class SCNScaffold extends StatelessWidget {
|
||||
showThemeSwitch: showThemeSwitch,
|
||||
showDebug: showDebug,
|
||||
showSearch: showSearch,
|
||||
showShare: showShare,
|
||||
onShare: onShare ?? () {},
|
||||
),
|
||||
body: child,
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -17,15 +19,24 @@ import 'firebase_options.dart';
|
||||
void main() async {
|
||||
print('[INIT] Application starting...');
|
||||
|
||||
print('[INIT] Ensure WidgetsFlutterBinding...');
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await Hive.initFlutter();
|
||||
print('[INIT] Init Globals...');
|
||||
|
||||
await Globals().init();
|
||||
|
||||
print('[INIT] Init Hive...');
|
||||
|
||||
await Hive.initFlutter();
|
||||
|
||||
Hive.registerAdapter(SCNRequestAdapter());
|
||||
Hive.registerAdapter(SCNLogAdapter());
|
||||
Hive.registerAdapter(SCNLogLevelAdapter());
|
||||
|
||||
print('[INIT] Load Hive<scn-requests>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<SCNRequest>('scn-requests');
|
||||
} catch (exc, trace) {
|
||||
@ -34,6 +45,8 @@ void main() async {
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-logs>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<SCNLog>('scn-logs');
|
||||
} catch (exc, trace) {
|
||||
@ -42,45 +55,58 @@ void main() async {
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load AppAuth...');
|
||||
|
||||
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
||||
|
||||
if (appAuth.isAuth()) {
|
||||
try {
|
||||
print('[INIT] Load User...');
|
||||
await appAuth.loadUser();
|
||||
//TODO fallback to cached user (perhaps event use cached client (if exists) directly and only update/load in background)
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace);
|
||||
}
|
||||
try {
|
||||
print('[INIT] Load Client...');
|
||||
await appAuth.loadClient();
|
||||
//TODO fallback to cached client (perhaps event use cached client (if exists) directly and only update/load in background)
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
if (!Platform.isLinux) {
|
||||
print('[INIT] Init Firebase...');
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
|
||||
await FirebaseMessaging.instance.requestPermission(provisional: true);
|
||||
print('[INIT] Request Notification permissions...');
|
||||
await FirebaseMessaging.instance.requestPermission(provisional: true);
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
|
||||
try {
|
||||
setFirebaseToken(fcmToken);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to set firebase token: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}).onError((dynamic err) {
|
||||
ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? ''));
|
||||
});
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
|
||||
try {
|
||||
setFirebaseToken(fcmToken);
|
||||
print('[INIT] Query firebase token...');
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
if (fcmToken != null) {
|
||||
setFirebaseToken(fcmToken);
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to set firebase token: ' + exc.toString(), trace: trace);
|
||||
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}).onError((dynamic err) {
|
||||
ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? ''));
|
||||
});
|
||||
|
||||
try {
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
if (fcmToken != null) {
|
||||
setFirebaseToken(fcmToken);
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
|
||||
} else {
|
||||
print('[INIT] Skip Firebase init (Platform == Linux)...');
|
||||
}
|
||||
|
||||
ApplicationLog.debug('Application started');
|
||||
ApplicationLog.debug('[INIT] Application started');
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
@ -112,7 +138,7 @@ void setFirebaseToken(String fcmToken) async {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldToken != null && oldToken == fcmToken && client != null && client!.fcmToken == fcmToken) {
|
||||
if (oldToken != null && oldToken == fcmToken && client != null && client.fcmToken == fcmToken) {
|
||||
ApplicationLog.info('Firebase token unchanged - do nothing', additional: 'Token: $fcmToken');
|
||||
return;
|
||||
}
|
||||
|
@ -1,9 +1,61 @@
|
||||
class APIError {
|
||||
final String success;
|
||||
final String error;
|
||||
final bool success;
|
||||
final int error;
|
||||
final String errhighlight;
|
||||
final String message;
|
||||
|
||||
static final MISSING_UID = 1101;
|
||||
static final MISSING_TOK = 1102;
|
||||
static final MISSING_TITLE = 1103;
|
||||
static final INVALID_PRIO = 1104;
|
||||
static final REQ_METHOD = 1105;
|
||||
static final INVALID_CLIENTTYPE = 1106;
|
||||
static final PAGETOKEN_ERROR = 1121;
|
||||
static final BINDFAIL_QUERY_PARAM = 1151;
|
||||
static final BINDFAIL_BODY_PARAM = 1152;
|
||||
static final BINDFAIL_URI_PARAM = 1153;
|
||||
static final INVALID_BODY_PARAM = 1161;
|
||||
static final INVALID_ENUM_VALUE = 1171;
|
||||
|
||||
static final NO_TITLE = 1201;
|
||||
static final TITLE_TOO_LONG = 1202;
|
||||
static final CONTENT_TOO_LONG = 1203;
|
||||
static final USR_MSG_ID_TOO_LONG = 1204;
|
||||
static final TIMESTAMP_OUT_OF_RANGE = 1205;
|
||||
static final SENDERNAME_TOO_LONG = 1206;
|
||||
static final CHANNEL_TOO_LONG = 1207;
|
||||
static final CHANNEL_DESCRIPTION_TOO_LONG = 1208;
|
||||
static final CHANNEL_NAME_EMPTY = 1209;
|
||||
|
||||
static final USER_NOT_FOUND = 1301;
|
||||
static final CLIENT_NOT_FOUND = 1302;
|
||||
static final CHANNEL_NOT_FOUND = 1303;
|
||||
static final SUBSCRIPTION_NOT_FOUND = 1304;
|
||||
static final MESSAGE_NOT_FOUND = 1305;
|
||||
static final SUBSCRIPTION_USER_MISMATCH = 1306;
|
||||
static final KEY_NOT_FOUND = 1307;
|
||||
static final USER_AUTH_FAILED = 1311;
|
||||
|
||||
static final NO_DEVICE_LINKED = 1401;
|
||||
|
||||
static final CHANNEL_ALREADY_EXISTS = 1501;
|
||||
static final CANNOT_SELFDELETE_KEY = 1511;
|
||||
static final CANNOT_SELFUPDATE_KEY = 1512;
|
||||
|
||||
static final QUOTA_REACHED = 2101;
|
||||
|
||||
static final FAILED_VERIFY_PRO_TOKEN = 3001;
|
||||
static final INVALID_PRO_TOKEN = 3002;
|
||||
|
||||
static final COMMIT_FAILED = 9001;
|
||||
static final DATABASE_ERROR = 9002;
|
||||
static final PERM_QUERY_FAIL = 9003;
|
||||
static final FIREBASE_COM_FAILED = 9901;
|
||||
static final FIREBASE_COM_ERRORED = 9902;
|
||||
static final INTERNAL_EXCEPTION = 9903;
|
||||
static final PANIC = 9904;
|
||||
static final NOT_IMPLEMENTED = 9905;
|
||||
|
||||
const APIError({
|
||||
required this.success,
|
||||
required this.error,
|
||||
@ -13,8 +65,8 @@ class APIError {
|
||||
|
||||
factory APIError.fromJson(Map<String, dynamic> json) {
|
||||
return APIError(
|
||||
success: json['success'] as String,
|
||||
error: json['error'] as String,
|
||||
success: json['success'] as bool,
|
||||
error: (json['error'] as double).toInt(),
|
||||
errhighlight: json['errhighlight'] as String,
|
||||
message: json['message'] as String,
|
||||
);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lazy_indexed_stack/flutter_lazy_indexed_stack.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/components/hidable_fab/hidable_fab.dart';
|
||||
@ -62,15 +61,16 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
|
||||
title: null,
|
||||
showDebug: true,
|
||||
showSearch: _selectedIndex == 0 || _selectedIndex == 1,
|
||||
showShare: false,
|
||||
showThemeSwitch: true,
|
||||
),
|
||||
body: LazyIndexedStack(
|
||||
body: IndexedStack(
|
||||
children: [
|
||||
MessageListPage(),
|
||||
ChannelRootPage(),
|
||||
AccountRootPage(),
|
||||
SettingsRootPage(),
|
||||
SendRootPage(),
|
||||
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage()),
|
||||
],
|
||||
index: _selectedIndex,
|
||||
),
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
@ -9,6 +11,8 @@ import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AccountRootPage extends StatefulWidget {
|
||||
const AccountRootPage({super.key});
|
||||
@ -140,22 +144,23 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)),
|
||||
UI.button(
|
||||
text: 'Create new account',
|
||||
onPressed: () {
|
||||
if (loading) return;
|
||||
_createNewAccount();
|
||||
},
|
||||
child: const Text('Create new account'),
|
||||
big: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)),
|
||||
UI.button(
|
||||
text: 'Use existing account',
|
||||
onPressed: () {
|
||||
if (loading) return;
|
||||
Navigator.push(context, MaterialPageRoute<AccountLoginPage>(builder: (context) => AccountLoginPage()));
|
||||
},
|
||||
child: const Text('Use existing account'),
|
||||
tonal: true,
|
||||
big: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -164,28 +169,22 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
}
|
||||
|
||||
Widget _buildShowAccount(BuildContext context, AppAuth acc, User user) {
|
||||
//TODO better layout
|
||||
return Column(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(context, user),
|
||||
const SizedBox(height: 16),
|
||||
Text(user.username ?? user.userID, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
const SizedBox(height: 16),
|
||||
..._buildCards(context, user),
|
||||
],
|
||||
),
|
||||
),
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(context, user),
|
||||
const SizedBox(height: 16),
|
||||
Text(user.username ?? user.userID, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
const SizedBox(height: 16),
|
||||
..._buildCards(context, user),
|
||||
SizedBox(height: 16),
|
||||
_buildFooter(context, user),
|
||||
SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
const Expanded(child: SizedBox(height: 16)),
|
||||
_buildFooter(context, user),
|
||||
SizedBox(height: 40)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -272,23 +271,15 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.pen),
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: BoxConstraints(),
|
||||
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||
UI.buttonIconOnly(
|
||||
onPressed: () {/*TODO*/},
|
||||
icon: FontAwesomeIcons.pen,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (!user.isPro)
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.cartCircleArrowUp),
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: BoxConstraints(),
|
||||
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||
UI.buttonIconOnly(
|
||||
onPressed: () {/*TODO*/},
|
||||
icon: FontAwesomeIcons.cartCircleArrowUp,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -298,132 +289,97 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
|
||||
List<Widget> _buildCards(BuildContext context, User user) {
|
||||
return [
|
||||
Card.filled(
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
onTap: () {/*TODO*/},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureSubscriptionCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Subscriptions', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureSubscriptionCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Subscriptions', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
Card.filled(
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
onTap: () {/*TODO*/},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureClientCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Clients', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureClientCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Clients', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
Card.filled(
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
onTap: () {/*TODO*/},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureKeyCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Keys', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureKeyCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Keys', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
Card.filled(
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
onTap: () {/*TODO*/},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureChannelSubscribedCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Channels', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureChannelSubscribedCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Channels', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
Card.filled(
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
onTap: () {/*TODO*/},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('${user.messagesSent}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
const SizedBox(width: 12),
|
||||
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('${user.messagesSent}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
const SizedBox(width: 12),
|
||||
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
];
|
||||
}
|
||||
@ -433,9 +389,19 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: FilledButton(onPressed: _logout, child: Text('Logout'), style: TextButton.styleFrom(backgroundColor: Colors.orange))),
|
||||
Expanded(
|
||||
child: UI.button(
|
||||
text: 'Logout',
|
||||
onPressed: _logout,
|
||||
color: Colors.orange,
|
||||
)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: FilledButton(onPressed: _deleteAccount, child: Text('Delete Account'), style: TextButton.styleFrom(backgroundColor: Colors.red))),
|
||||
Expanded(
|
||||
child: UI.button(
|
||||
text: 'Delete Account',
|
||||
onPressed: _deleteAccount,
|
||||
color: Colors.red,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -447,15 +413,21 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
try {
|
||||
final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true);
|
||||
final String? fcmToken;
|
||||
if (Platform.isLinux) {
|
||||
Toaster.warn("Unsupported Platform", 'Your platform is not supported by FCM - notifications will not work');
|
||||
fcmToken = '(linux-' + Uuid().v4() + ')';
|
||||
} else {
|
||||
final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true);
|
||||
|
||||
if (notificationSettings.authorizationStatus == AuthorizationStatus.denied) {
|
||||
Toaster.error("Missing Permission", 'Please allow notifications to create an account');
|
||||
return;
|
||||
if (notificationSettings.authorizationStatus == AuthorizationStatus.denied) {
|
||||
Toaster.error("Missing Permission", 'Please allow notifications to create an account');
|
||||
return;
|
||||
}
|
||||
|
||||
fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
}
|
||||
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
|
||||
if (fcmToken == null) {
|
||||
Toaster.warn("Missing Token", 'No FCM Token found, please allow notifications, ensure you have a network connection and restart the app');
|
||||
return;
|
||||
|
@ -9,6 +9,7 @@ import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/token_source.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class AccountLoginPage extends StatefulWidget {
|
||||
const AccountLoginPage({super.key});
|
||||
@ -102,10 +103,10 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)),
|
||||
UI.button(
|
||||
text: 'Login',
|
||||
big: true,
|
||||
onPressed: _login,
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class DebugActionsPage extends StatefulWidget {
|
||||
@override
|
||||
@ -17,36 +17,40 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Toaster.success("Hello World", "This was a triumph!"),
|
||||
child: const Text('Show Success Notification'),
|
||||
text: 'Show Success Notification',
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
|
||||
SizedBox(height: 4),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Toaster.info("Hello World", "This was a triumph!"),
|
||||
child: const Text('Show Info Notification'),
|
||||
text: 'Show Info Notification',
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
|
||||
SizedBox(height: 4),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Toaster.warn("Hello World", "This was a triumph!"),
|
||||
child: const Text('Show Warn Notification'),
|
||||
text: 'Show Warn Notification',
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
|
||||
SizedBox(height: 4),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Toaster.error("Hello World", "This was a triumph!"),
|
||||
child: const Text('Show Info Notification'),
|
||||
text: 'Show Info Notification',
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
|
||||
SizedBox(height: 4),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Toaster.simple("Hello World"),
|
||||
child: const Text('Show Simple Notification'),
|
||||
text: 'Show Simple Notification',
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: _sendTokenToServer,
|
||||
child: const Text('Send FCM Token to Server'),
|
||||
text: 'Send FCM Token to Server',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class DebugRequestViewPage extends StatelessWidget {
|
||||
final SCNRequest request;
|
||||
@ -55,17 +56,13 @@ class DebugRequestViewPage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
IconButton(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.copy,
|
||||
),
|
||||
UI.buttonIconOnly(
|
||||
iconSize: 14,
|
||||
padding: EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||
constraints: BoxConstraints(),
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: value));
|
||||
Clipboard.setData(new ClipboardData(text: title));
|
||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
||||
},
|
||||
icon: FontAwesomeIcons.copy,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -12,6 +12,9 @@ import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
||||
class MessageListPage extends StatefulWidget {
|
||||
const MessageListPage({super.key});
|
||||
|
||||
//TODO reload on switch to tab
|
||||
//TODO reload on app to foreground
|
||||
|
||||
@override
|
||||
State<MessageListPage> createState() => _MessageListPageState();
|
||||
}
|
||||
@ -25,6 +28,7 @@ class _MessageListPageState extends State<MessageListPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//TODO init with state from cache - also allow tho show cache on error
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class MessageListItem extends StatelessWidget {
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
@ -102,19 +103,10 @@ class MessageListItem extends StatelessWidget {
|
||||
if (message.priority == 2) SizedBox(width: 4),
|
||||
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
|
||||
if (message.priority == 0) SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(4, 0, 4, 0),
|
||||
margin: const EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).hintColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
child: Text(
|
||||
resolveChannelName(message),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).cardColor, fontSize: 12),
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
UI.channelChip(
|
||||
context: context,
|
||||
text: resolveChannelName(message),
|
||||
margin: EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||
),
|
||||
Expanded(child: SizedBox()),
|
||||
Text(
|
||||
|
@ -1,9 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/api_error.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class MessageViewPage extends StatefulWidget {
|
||||
const MessageViewPage({super.key, required this.message});
|
||||
@ -15,18 +25,54 @@ class MessageViewPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MessageViewPageState extends State<MessageViewPage> {
|
||||
late Future<Message>? futureMessage;
|
||||
late Future<(Message, ChannelWithSubscription?, KeyToken?)>? mainFuture;
|
||||
(Message, ChannelWithSubscription?, KeyToken?)? mainFutureSnapshot = null;
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
|
||||
bool _monospaceMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
futureMessage = fetchMessage();
|
||||
mainFuture = fetchData();
|
||||
}
|
||||
|
||||
Future<Message> fetchMessage() async {
|
||||
Future<(Message, ChannelWithSubscription?, KeyToken?)> fetchData() async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
return await APIClient.getMessage(acc, widget.message.messageID);
|
||||
final msg = await APIClient.getMessage(acc, widget.message.messageID);
|
||||
|
||||
ChannelWithSubscription? chn = null;
|
||||
try {
|
||||
chn = await APIClient.getChannel(acc, msg.channelID); //TODO getShortChannel (?) -> no perm
|
||||
} on APIException catch (e) {
|
||||
if (e.error == APIError.USER_AUTH_FAILED) {
|
||||
chn = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
KeyToken? tok = null;
|
||||
try {
|
||||
tok = await APIClient.getKeyToken(acc, msg.usedKeyID); //TODO getShortKeyToken (?) -> no perm
|
||||
} on APIException catch (e) {
|
||||
if (e.error == APIError.USER_AUTH_FAILED) {
|
||||
tok = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO getShortUser for sender
|
||||
|
||||
//await Future.delayed(const Duration(seconds: 2), () {});
|
||||
|
||||
final r = (msg, chn, tok);
|
||||
|
||||
mainFutureSnapshot = r;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -39,15 +85,18 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
return SCNScaffold(
|
||||
title: 'Message',
|
||||
showSearch: false,
|
||||
child: FutureBuilder<Message>(
|
||||
future: futureMessage,
|
||||
showShare: true,
|
||||
onShare: _share,
|
||||
child: FutureBuilder<(Message, ChannelWithSubscription?, KeyToken?)>(
|
||||
future: mainFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return buildMessageView(snapshot.data!, false);
|
||||
final (msg, chn, tok) = snapshot.data!;
|
||||
return _buildMessageView(context, msg, chn, tok, false);
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text('${snapshot.error}')); //TODO nice error page
|
||||
} else if (!widget.message.trimmed) {
|
||||
return buildMessageView(widget.message, true);
|
||||
return _buildMessageView(context, widget.message, null, null, true);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
@ -56,15 +105,172 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildMessageView(Message message, bool loading) {
|
||||
//TODO loading true/false indicator
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(message.title),
|
||||
Text(message.content ?? ''),
|
||||
],
|
||||
void _share() async {
|
||||
var msg = widget.message;
|
||||
if (mainFutureSnapshot != null) {
|
||||
(msg, _, _) = mainFutureSnapshot!;
|
||||
}
|
||||
|
||||
if (msg.content != null) {
|
||||
final result = await Share.share(msg.content!, subject: msg.title);
|
||||
|
||||
if (result.status == ShareResultStatus.unavailable) {
|
||||
Toaster.error('Error', "Failed to open share dialog");
|
||||
}
|
||||
} else {
|
||||
final result = await Share.share(msg.title);
|
||||
|
||||
if (result.status == ShareResultStatus.unavailable) {
|
||||
Toaster.error('Error', "Failed to open share dialog");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMessageView(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) {
|
||||
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
..._buildMessageHeader(context, message, channel, token, loading),
|
||||
SizedBox(height: 8),
|
||||
if (message.content != null) ..._buildMessageContent(context, message, channel, token),
|
||||
SizedBox(height: 8),
|
||||
if (message.senderName != null) _buildMetaCard(context, FontAwesomeIcons.solidSignature, 'Sender', [message.senderName!], () => {/*TODO*/}),
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidGearCode, 'KeyToken', [message.usedKeyID, if (token != null) token.name], () => {/*TODO*/}),
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidIdCardClip, 'MessageID', [message.messageID, if (message.userMessageID != null) message.userMessageID!], null),
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel?.channel.displayName ?? message.channelInternalName], () => {/*TODO*/}),
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidTimer, 'Timestamp', [message.timestamp], null),
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidUser, 'User', ['TODO'], () => {/*TODO*/}), //TODO
|
||||
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveChannelName(ChannelWithSubscription? channel, Message message) {
|
||||
return channel?.channel.displayName ?? message.channelInternalName;
|
||||
}
|
||||
|
||||
List<Widget> _buildMessageHeader(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) {
|
||||
return [
|
||||
Row(
|
||||
children: [
|
||||
UI.channelChip(
|
||||
context: context,
|
||||
text: _resolveChannelName(channel, message),
|
||||
margin: const EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||
fontSize: 16,
|
||||
),
|
||||
Expanded(child: SizedBox()),
|
||||
Text(_dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
if (!loading) Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
if (loading)
|
||||
Stack(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(child: Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))),
|
||||
SizedBox(height: 20, width: 20),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: SizedBox(width: 0)),
|
||||
SizedBox(child: CircularProgressIndicator(), height: 20, width: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildMessageContent(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) {
|
||||
return [
|
||||
Row(
|
||||
children: [
|
||||
if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]),
|
||||
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
|
||||
Expanded(child: SizedBox()),
|
||||
UI.buttonIconOnly(
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: message.content ?? ''));
|
||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
||||
},
|
||||
icon: FontAwesomeIcons.copy,
|
||||
),
|
||||
UI.buttonIconOnly(
|
||||
icon: _monospaceMode ? FontAwesomeIcons.lineColumns : FontAwesomeIcons.alignLeft,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_monospaceMode = !_monospaceMode;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
_monospaceMode
|
||||
? UI.box(
|
||||
context: context,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Text(
|
||||
message.content ?? '',
|
||||
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"]),
|
||||
),
|
||||
),
|
||||
borderColor: (message.priority == 2) ? Colors.red[900] : null,
|
||||
)
|
||||
: UI.box(
|
||||
context: context,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Text(message.content ?? ''),
|
||||
borderColor: (message.priority == 2) ? Colors.red[900] : null,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildMetaCard(BuildContext context, IconData icn, String title, List<String> values, void Function()? action) {
|
||||
final container = UI.box(
|
||||
context: context,
|
||||
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
||||
child: Row(
|
||||
children: [
|
||||
FaIcon(icn, size: 18),
|
||||
SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
for (final val in values) Text(val, style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (action == null) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||
child: container,
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
onTap: action,
|
||||
child: container,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/models/client.dart';
|
||||
@ -63,6 +62,8 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
||||
}
|
||||
|
||||
void load() {
|
||||
//final cdat = Globals().sharedPrefs.getString('auth.cdate');
|
||||
//final mdat = Globals().sharedPrefs.getString('auth.mdate');
|
||||
final uid = Globals().sharedPrefs.getString('auth.userid');
|
||||
final cid = Globals().sharedPrefs.getString('auth.clientid');
|
||||
final toka = Globals().sharedPrefs.getString('auth.tokenadmin');
|
||||
@ -85,17 +86,23 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (_clientID == null || _userID == null || _tokenAdmin == null || _tokenSend == null) {
|
||||
await prefs.remove('auth.userid');
|
||||
await prefs.remove('auth.tokenadmin');
|
||||
await prefs.remove('auth.tokensend');
|
||||
await Globals().sharedPrefs.remove('auth.userid');
|
||||
await Globals().sharedPrefs.remove('auth.clientid');
|
||||
await Globals().sharedPrefs.remove('auth.tokenadmin');
|
||||
await Globals().sharedPrefs.remove('auth.tokensend');
|
||||
await Globals().sharedPrefs.setString('auth.cdate', "");
|
||||
await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
|
||||
} else {
|
||||
await prefs.setString('auth.userid', _userID!);
|
||||
await prefs.setString('auth.clientid', _clientID!);
|
||||
await prefs.setString('auth.tokenadmin', _tokenAdmin!);
|
||||
await prefs.setString('auth.tokensend', _tokenSend!);
|
||||
await Globals().sharedPrefs.setString('auth.userid', _userID!);
|
||||
await Globals().sharedPrefs.setString('auth.clientid', _clientID!);
|
||||
await Globals().sharedPrefs.setString('auth.tokenadmin', _tokenAdmin!);
|
||||
await Globals().sharedPrefs.setString('auth.tokensend', _tokenSend!);
|
||||
if (Globals().sharedPrefs.getString('auth.cdate') == null) await Globals().sharedPrefs.setString('auth.cdate', DateTime.now().toIso8601String());
|
||||
await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
|
||||
}
|
||||
|
||||
Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
|
||||
}
|
||||
|
||||
Future<User> loadUser({bool force = false}) async {
|
||||
|
1
flutter/lib/utils/navi.dart
Normal file
1
flutter/lib/utils/navi.dart
Normal file
@ -0,0 +1 @@
|
||||
class Navi {}
|
109
flutter/lib/utils/ui.dart
Normal file
109
flutter/lib/utils/ui.dart
Normal file
@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class UI {
|
||||
static const double DefaultBorderRadius = 4;
|
||||
|
||||
static Widget button({required String text, required void Function() onPressed, bool big = false, Color? color = null, bool tonal = false, IconData? icon = null}) {
|
||||
final double fontSize = big ? 24 : 14;
|
||||
final padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null;
|
||||
|
||||
final style = FilledButton.styleFrom(
|
||||
textStyle: TextStyle(fontSize: fontSize),
|
||||
padding: padding,
|
||||
backgroundColor: color,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
|
||||
);
|
||||
|
||||
if (tonal) {
|
||||
if (icon != null) {
|
||||
return FilledButton.tonalIcon(
|
||||
style: style,
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon),
|
||||
label: Text(text),
|
||||
);
|
||||
} else {
|
||||
return FilledButton.tonal(
|
||||
style: style,
|
||||
onPressed: onPressed,
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (icon != null) {
|
||||
return FilledButton.icon(
|
||||
style: style,
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon),
|
||||
label: Text(text),
|
||||
);
|
||||
} else {
|
||||
return FilledButton(
|
||||
style: style,
|
||||
onPressed: onPressed,
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Widget buttonIconOnly({
|
||||
required void Function() onPressed,
|
||||
required IconData icon,
|
||||
double? iconSize = null,
|
||||
}) {
|
||||
return IconButton(
|
||||
icon: FaIcon(icon),
|
||||
iconSize: iconSize ?? 18,
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: BoxConstraints(),
|
||||
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget buttonCard({required BuildContext context, required Widget child, required void Function() onTap, EdgeInsets? margin = null}) {
|
||||
return Card.filled(
|
||||
margin: margin,
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget channelChip({required BuildContext context, required String text, EdgeInsets? margin = null, double fontSize = 12}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(4, 0, 4, 0),
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).hintColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(DefaultBorderRadius)),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).cardColor, fontSize: fontSize),
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget box({required BuildContext context, required Widget child, required EdgeInsets? padding, Color? borderColor = null}) {
|
||||
return Container(
|
||||
padding: padding ?? EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: borderColor ?? Theme.of(context).hintColor),
|
||||
borderRadius: BorderRadius.circular(DefaultBorderRadius),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import firebase_core
|
||||
import firebase_messaging
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
@ -161,6 +161,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -687,6 +695,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -941,7 +965,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
|
||||
|
@ -29,6 +29,8 @@ dependencies:
|
||||
firebase_messaging: ^14.9.4
|
||||
device_info_plus: ^10.1.0
|
||||
toastification: ^2.0.0
|
||||
uuid: ^4.4.0
|
||||
share_plus: ^9.0.0
|
||||
|
||||
|
||||
dependency_overrides:
|
||||
|
@ -7,11 +7,14 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
firebase_core
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
127
scnserver/api/handler/apiPreview.go
Normal file
127
scnserver/api/handler/apiPreview.go
Normal file
@ -0,0 +1,127 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// GetUserPreview swaggerdoc
|
||||
//
|
||||
// @Summary Get a user (similar to api-user-get, but can be called from anyone and only returns a subset of fields)
|
||||
// @ID api-user-get-preview
|
||||
// @Tags API-v2
|
||||
//
|
||||
// @Param uid path string true "UserID"
|
||||
//
|
||||
// @Success 200 {object} models.UserPreviewJSON
|
||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||
// @Failure 404 {object} ginresp.apiError "user not found"
|
||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||
//
|
||||
// @Router /api/v2/preview/users/{uid} [GET]
|
||||
func (h APIHandler) GetUserPreview(g *gin.Context) ginresp.HTTPResponse {
|
||||
type uri struct {
|
||||
UserID models.UserID `uri:"uid" binding:"entityid"`
|
||||
}
|
||||
|
||||
var u uri
|
||||
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
}
|
||||
defer ctx.Cancel()
|
||||
|
||||
user, err := h.database.GetUser(ctx, u.UserID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
|
||||
}
|
||||
|
||||
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSONPreview()))
|
||||
}
|
||||
|
||||
// GetChannelPreview swaggerdoc
|
||||
//
|
||||
// @Summary Get a single channel (similar to api-channels-get, but can be called from anyone and only returns a subset of fields)
|
||||
// @ID api-channels-get-preview
|
||||
// @Tags API-v2
|
||||
//
|
||||
// @Param cid path string true "ChannelID"
|
||||
//
|
||||
// @Success 200 {object} models.ChannelPreviewJSON
|
||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||
// @Failure 404 {object} ginresp.apiError "channel not found"
|
||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||
//
|
||||
// @Router /api/v2/preview/channels/{cid} [GET]
|
||||
func (h APIHandler) GetChannelPreview(g *gin.Context) ginresp.HTTPResponse {
|
||||
type uri struct {
|
||||
UserID models.UserID `uri:"uid" binding:"entityid"`
|
||||
ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
|
||||
}
|
||||
|
||||
var u uri
|
||||
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
}
|
||||
defer ctx.Cancel()
|
||||
|
||||
channel, err := h.database.GetChannelByID(ctx, u.ChannelID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
|
||||
}
|
||||
|
||||
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSONPreview()))
|
||||
}
|
||||
|
||||
// GetUserKeyPreview swaggerdoc
|
||||
//
|
||||
// @Summary Get a single key (similar to api-tokenkeys-get, but can be called from anyone and only returns a subset of fields)
|
||||
// @ID api-tokenkeys-get-preview
|
||||
// @Tags API-v2
|
||||
//
|
||||
// @Param kid path string true "TokenKeyID"
|
||||
//
|
||||
// @Success 200 {object} models.KeyTokenPreviewJSON
|
||||
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
|
||||
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
|
||||
// @Failure 404 {object} ginresp.apiError "message not found"
|
||||
// @Failure 500 {object} ginresp.apiError "internal server error"
|
||||
//
|
||||
// @Router /api/v2/preview/keys/{kid} [GET]
|
||||
func (h APIHandler) GetUserKeyPreview(g *gin.Context) ginresp.HTTPResponse {
|
||||
type uri struct {
|
||||
UserID models.UserID `uri:"uid" binding:"entityid"`
|
||||
KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
|
||||
}
|
||||
|
||||
var u uri
|
||||
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
}
|
||||
defer ctx.Cancel()
|
||||
|
||||
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||
}
|
||||
if err != nil {
|
||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
||||
}
|
||||
|
||||
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, keytoken.JSONPreview()))
|
||||
}
|
@ -157,6 +157,10 @@ func (r *Router) Init(e *gin.Engine) error {
|
||||
apiv2.GET("/messages", r.Wrap(r.apiHandler.ListMessages))
|
||||
apiv2.GET("/messages/:mid", r.Wrap(r.apiHandler.GetMessage))
|
||||
apiv2.DELETE("/messages/:mid", r.Wrap(r.apiHandler.DeleteMessage))
|
||||
|
||||
apiv2.GET("/preview/users/:uid", r.Wrap(r.apiHandler.GetUserPreview))
|
||||
apiv2.GET("/preview/keys/:kid", r.Wrap(r.apiHandler.GetUserKeyPreview))
|
||||
apiv2.GET("/preview/channels/:cid", r.Wrap(r.apiHandler.GetChannelPreview))
|
||||
}
|
||||
|
||||
// ================ Send API (unversioned) ================
|
||||
|
@ -41,6 +41,16 @@ func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
|
||||
}
|
||||
}
|
||||
|
||||
func (c Channel) JSONPreview() ChannelPreviewJSON {
|
||||
return ChannelPreviewJSON{
|
||||
ChannelID: c.ChannelID,
|
||||
OwnerUserID: c.OwnerUserID,
|
||||
InternalName: c.InternalName,
|
||||
DisplayName: c.DisplayName,
|
||||
DescriptionName: c.DescriptionName,
|
||||
}
|
||||
}
|
||||
|
||||
type ChannelWithSubscription struct {
|
||||
Channel
|
||||
Subscription *Subscription
|
||||
@ -74,6 +84,14 @@ type ChannelWithSubscriptionJSON struct {
|
||||
Subscription *SubscriptionJSON `json:"subscription"`
|
||||
}
|
||||
|
||||
type ChannelPreviewJSON struct {
|
||||
ChannelID ChannelID `json:"channel_id"`
|
||||
OwnerUserID UserID `json:"owner_user_id"`
|
||||
InternalName string `json:"internal_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
DescriptionName *string `json:"description_name"`
|
||||
}
|
||||
|
||||
type ChannelDB struct {
|
||||
ChannelID ChannelID `db:"channel_id"`
|
||||
OwnerUserID UserID `db:"owner_user_id"`
|
||||
|
@ -5,7 +5,7 @@ package models
|
||||
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
import "gogs.mikescher.com/BlackForestBytes/goext/enums"
|
||||
|
||||
const ChecksumEnumGenerator = "5b115c5f107801af608630d2c5adce57cd4b050d176c8cd3db5c132020bf153c" // GoExtVersion: 0.0.463
|
||||
const ChecksumEnumGenerator = "e500346e3f60b3abf78558ec3df128c3be2a1cefa71c4f1feba9293d14eb85d1" // GoExtVersion: 0.0.463
|
||||
|
||||
// ================================ ClientType ================================
|
||||
//
|
||||
|
@ -15,7 +15,7 @@ import "reflect"
|
||||
import "regexp"
|
||||
import "strings"
|
||||
|
||||
const ChecksumCharsetIDGenerator = "5b115c5f107801af608630d2c5adce57cd4b050d176c8cd3db5c132020bf153c" // GoExtVersion: 0.0.463
|
||||
const ChecksumCharsetIDGenerator = "e500346e3f60b3abf78558ec3df128c3be2a1cefa71c4f1feba9293d14eb85d1" // GoExtVersion: 0.0.463
|
||||
|
||||
const idlen = 24
|
||||
|
||||
|
@ -92,6 +92,17 @@ func (k KeyToken) JSON() KeyTokenJSON {
|
||||
}
|
||||
}
|
||||
|
||||
func (k KeyToken) JSONPreview() KeyTokenPreviewJSON {
|
||||
return KeyTokenPreviewJSON{
|
||||
KeyTokenID: k.KeyTokenID,
|
||||
Name: k.Name,
|
||||
OwnerUserID: k.OwnerUserID,
|
||||
AllChannels: k.AllChannels,
|
||||
Channels: k.Channels,
|
||||
Permissions: k.Permissions.String(),
|
||||
}
|
||||
}
|
||||
|
||||
type KeyTokenJSON struct {
|
||||
KeyTokenID KeyTokenID `json:"keytoken_id"`
|
||||
Name string `json:"name"`
|
||||
@ -109,6 +120,15 @@ type KeyTokenWithTokenJSON struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type KeyTokenPreviewJSON struct {
|
||||
KeyTokenID KeyTokenID `json:"keytoken_id"`
|
||||
Name string `json:"name"`
|
||||
OwnerUserID UserID `json:"owner_user_id"`
|
||||
AllChannels bool `json:"all_channels"`
|
||||
Channels []ChannelID `json:"channels"`
|
||||
Permissions string `json:"permissions"`
|
||||
}
|
||||
|
||||
func (j KeyTokenJSON) WithToken(tok string) KeyTokenWithTokenJSON {
|
||||
return KeyTokenWithTokenJSON{
|
||||
KeyTokenJSON: j,
|
||||
|
@ -116,6 +116,13 @@ func (u User) MaxTimestampDiffHours() int {
|
||||
return 24
|
||||
}
|
||||
|
||||
func (u User) JSONPreview() UserPreviewJSON {
|
||||
return UserPreviewJSON{
|
||||
UserID: u.UserID,
|
||||
Username: u.Username,
|
||||
}
|
||||
}
|
||||
|
||||
type UserJSON struct {
|
||||
UserID UserID `json:"user_id"`
|
||||
Username *string `json:"username"`
|
||||
@ -137,6 +144,11 @@ type UserJSON struct {
|
||||
MaxUserMessageIDLength int `json:"max_user_message_id_length"`
|
||||
}
|
||||
|
||||
type UserPreviewJSON struct {
|
||||
UserID UserID `json:"user_id"`
|
||||
Username *string `json:"username"`
|
||||
}
|
||||
|
||||
type UserJSONWithClientsAndKeys struct {
|
||||
UserJSON
|
||||
Clients []ClientJSON `json:"clients"`
|
||||
|
@ -19,37 +19,61 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "test",
|
||||
"name": "channel",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "This is a message",
|
||||
"name": "content",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "P3TNH8mvv14fm",
|
||||
"name": "key",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "db8b0e6a-a08c-4646",
|
||||
"name": "msg_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"type": "integer",
|
||||
"example": 1,
|
||||
"name": "priority",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "example-server",
|
||||
"name": "sender_name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"example": 1669824037,
|
||||
"name": "timestamp",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "Hello World",
|
||||
"name": "title",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_key",
|
||||
"example": "7725",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@ -62,37 +86,61 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "test",
|
||||
"name": "channel",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "This is a message",
|
||||
"name": "content",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "P3TNH8mvv14fm",
|
||||
"name": "key",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "db8b0e6a-a08c-4646",
|
||||
"name": "msg_id",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"type": "integer",
|
||||
"example": 1,
|
||||
"name": "priority",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "example-server",
|
||||
"name": "sender_name",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"example": 1669824037,
|
||||
"name": "timestamp",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "Hello World",
|
||||
"name": "title",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "user_id",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_key",
|
||||
"example": "7725",
|
||||
"name": "user_id",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
@ -1009,6 +1057,156 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/preview/channels/{cid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"API-v2"
|
||||
],
|
||||
"summary": "Get a single channel (similar to api-channels-get, but can be called from anyone and only returns a subset of fields)",
|
||||
"operationId": "api-channels-get-preview",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "ChannelID",
|
||||
"name": "cid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ChannelPreviewJSON"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "supplied values/parameters cannot be parsed / are invalid",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "user is not authorized / has missing permissions",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "channel not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/preview/keys/{kid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"API-v2"
|
||||
],
|
||||
"summary": "Get a single key (similar to api-tokenkeys-get, but can be called from anyone and only returns a subset of fields)",
|
||||
"operationId": "api-tokenkeys-get-preview",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "TokenKeyID",
|
||||
"name": "kid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.KeyTokenPreviewJSON"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "supplied values/parameters cannot be parsed / are invalid",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "user is not authorized / has missing permissions",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "message not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/preview/users/{uid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"API-v2"
|
||||
],
|
||||
"summary": "Get a user (similar to api-user-get, but can be called from anyone and only returns a subset of fields)",
|
||||
"operationId": "api-user-get-preview",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "UserID",
|
||||
"name": "uid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.UserPreviewJSON"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "supplied values/parameters cannot be parsed / are invalid",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "user is not authorized / has missing permissions",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "user not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/users": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -2567,37 +2765,61 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "test",
|
||||
"name": "channel",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "This is a message",
|
||||
"name": "content",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "P3TNH8mvv14fm",
|
||||
"name": "key",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "db8b0e6a-a08c-4646",
|
||||
"name": "msg_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"type": "integer",
|
||||
"example": 1,
|
||||
"name": "priority",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "example-server",
|
||||
"name": "sender_name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"example": 1669824037,
|
||||
"name": "timestamp",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "Hello World",
|
||||
"name": "title",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_key",
|
||||
"example": "7725",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@ -2610,37 +2832,61 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "test",
|
||||
"name": "channel",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "This is a message",
|
||||
"name": "content",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "P3TNH8mvv14fm",
|
||||
"name": "key",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "db8b0e6a-a08c-4646",
|
||||
"name": "msg_id",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"type": "integer",
|
||||
"example": 1,
|
||||
"name": "priority",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "example-server",
|
||||
"name": "sender_name",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"example": 1669824037,
|
||||
"name": "timestamp",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "Hello World",
|
||||
"name": "title",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "user_id",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_key",
|
||||
"example": "7725",
|
||||
"name": "user_id",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
@ -2689,72 +2935,120 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "test",
|
||||
"name": "channel",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "This is a message",
|
||||
"name": "content",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "P3TNH8mvv14fm",
|
||||
"name": "key",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "db8b0e6a-a08c-4646",
|
||||
"name": "msg_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"type": "integer",
|
||||
"example": 1,
|
||||
"name": "priority",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "example-server",
|
||||
"name": "sender_name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"example": 1669824037,
|
||||
"name": "timestamp",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "Hello World",
|
||||
"name": "title",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"type": "string",
|
||||
"example": "7725",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_key",
|
||||
"in": "query"
|
||||
"example": "test",
|
||||
"name": "channel",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "This is a message",
|
||||
"name": "content",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "P3TNH8mvv14fm",
|
||||
"name": "key",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "db8b0e6a-a08c-4646",
|
||||
"name": "msg_id",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"type": "integer",
|
||||
"example": 1,
|
||||
"name": "priority",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "example-server",
|
||||
"name": "sender_name",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"example": 1669824037,
|
||||
"name": "timestamp",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "Hello World",
|
||||
"name": "title",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "user_id",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_key",
|
||||
"example": "7725",
|
||||
"name": "user_id",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
@ -3251,26 +3545,46 @@
|
||||
"handler.SendMessage.combined": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"example": "test"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "This is a message"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"example": "P3TNH8mvv14fm"
|
||||
},
|
||||
"msg_id": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "db8b0e6a-a08c-4646"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"example": 1
|
||||
},
|
||||
"sender_name": {
|
||||
"type": "string",
|
||||
"example": "example-server"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
"type": "number",
|
||||
"example": 1669824037
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Hello World"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"user_key": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "7725"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -3299,7 +3613,7 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"scn_msg_id": {
|
||||
"type": "integer"
|
||||
"type": "string"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
@ -3487,6 +3801,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ChannelPreviewJSON": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"description_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"internal_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_user_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ChannelWithSubscriptionJSON": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -3630,6 +3964,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.KeyTokenPreviewJSON": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"all_channels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"channels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"keytoken_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.KeyTokenWithTokenJSON": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -3870,6 +4230,17 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.UserPreviewJSON": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
|
@ -327,19 +327,36 @@ definitions:
|
||||
type: object
|
||||
handler.SendMessage.combined:
|
||||
properties:
|
||||
channel:
|
||||
example: test
|
||||
type: string
|
||||
content:
|
||||
example: This is a message
|
||||
type: string
|
||||
key:
|
||||
example: P3TNH8mvv14fm
|
||||
type: string
|
||||
msg_id:
|
||||
example: db8b0e6a-a08c-4646
|
||||
type: string
|
||||
priority:
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
example: 1
|
||||
type: integer
|
||||
sender_name:
|
||||
example: example-server
|
||||
type: string
|
||||
timestamp:
|
||||
example: 1669824037
|
||||
type: number
|
||||
title:
|
||||
example: Hello World
|
||||
type: string
|
||||
user_id:
|
||||
type: integer
|
||||
user_key:
|
||||
example: "7725"
|
||||
type: string
|
||||
type: object
|
||||
handler.SendMessage.response:
|
||||
@ -359,7 +376,7 @@ definitions:
|
||||
quota_max:
|
||||
type: integer
|
||||
scn_msg_id:
|
||||
type: integer
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
suppress_send:
|
||||
@ -480,6 +497,19 @@ definitions:
|
||||
uri:
|
||||
type: string
|
||||
type: object
|
||||
models.ChannelPreviewJSON:
|
||||
properties:
|
||||
channel_id:
|
||||
type: string
|
||||
description_name:
|
||||
type: string
|
||||
display_name:
|
||||
type: string
|
||||
internal_name:
|
||||
type: string
|
||||
owner_user_id:
|
||||
type: string
|
||||
type: object
|
||||
models.ChannelWithSubscriptionJSON:
|
||||
properties:
|
||||
channel_id:
|
||||
@ -577,6 +607,23 @@ definitions:
|
||||
timestamp_lastused:
|
||||
type: string
|
||||
type: object
|
||||
models.KeyTokenPreviewJSON:
|
||||
properties:
|
||||
all_channels:
|
||||
type: boolean
|
||||
channels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
keytoken_id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
owner_user_id:
|
||||
type: string
|
||||
permissions:
|
||||
type: string
|
||||
type: object
|
||||
models.KeyTokenWithTokenJSON:
|
||||
properties:
|
||||
all_channels:
|
||||
@ -736,6 +783,13 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
models.UserPreviewJSON:
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
host: simplecloudnotifier.de
|
||||
info:
|
||||
contact: {}
|
||||
@ -748,52 +802,90 @@ paths:
|
||||
description: All parameter can be set via query-parameter or the json body.
|
||||
Only UserID, UserKey and Title are required
|
||||
parameters:
|
||||
- in: query
|
||||
- example: test
|
||||
in: query
|
||||
name: channel
|
||||
type: string
|
||||
- example: This is a message
|
||||
in: query
|
||||
name: content
|
||||
type: string
|
||||
- in: query
|
||||
- example: P3TNH8mvv14fm
|
||||
in: query
|
||||
name: key
|
||||
type: string
|
||||
- example: db8b0e6a-a08c-4646
|
||||
in: query
|
||||
name: msg_id
|
||||
type: string
|
||||
- in: query
|
||||
- enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
example: 1
|
||||
in: query
|
||||
name: priority
|
||||
type: integer
|
||||
- in: query
|
||||
- example: example-server
|
||||
in: query
|
||||
name: sender_name
|
||||
type: string
|
||||
- example: 1669824037
|
||||
in: query
|
||||
name: timestamp
|
||||
type: number
|
||||
- in: query
|
||||
- example: Hello World
|
||||
in: query
|
||||
name: title
|
||||
type: string
|
||||
- in: query
|
||||
- example: "7725"
|
||||
in: query
|
||||
name: user_id
|
||||
type: integer
|
||||
- in: query
|
||||
name: user_key
|
||||
type: string
|
||||
- description: ' '
|
||||
in: body
|
||||
name: post_body
|
||||
schema:
|
||||
$ref: '#/definitions/handler.SendMessage.combined'
|
||||
- in: formData
|
||||
- example: test
|
||||
in: formData
|
||||
name: channel
|
||||
type: string
|
||||
- example: This is a message
|
||||
in: formData
|
||||
name: content
|
||||
type: string
|
||||
- in: formData
|
||||
- example: P3TNH8mvv14fm
|
||||
in: formData
|
||||
name: key
|
||||
type: string
|
||||
- example: db8b0e6a-a08c-4646
|
||||
in: formData
|
||||
name: msg_id
|
||||
type: string
|
||||
- in: formData
|
||||
- enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
example: 1
|
||||
in: formData
|
||||
name: priority
|
||||
type: integer
|
||||
- in: formData
|
||||
- example: example-server
|
||||
in: formData
|
||||
name: sender_name
|
||||
type: string
|
||||
- example: 1669824037
|
||||
in: formData
|
||||
name: timestamp
|
||||
type: number
|
||||
- in: formData
|
||||
- example: Hello World
|
||||
in: formData
|
||||
name: title
|
||||
type: string
|
||||
- in: formData
|
||||
- example: "7725"
|
||||
in: formData
|
||||
name: user_id
|
||||
type: integer
|
||||
- in: formData
|
||||
name: user_key
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
@ -1422,6 +1514,108 @@ paths:
|
||||
summary: Get a single message (untrimmed)
|
||||
tags:
|
||||
- API-v2
|
||||
/api/v2/preview/channels/{cid}:
|
||||
get:
|
||||
operationId: api-channels-get-preview
|
||||
parameters:
|
||||
- description: ChannelID
|
||||
in: path
|
||||
name: cid
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.ChannelPreviewJSON'
|
||||
"400":
|
||||
description: supplied values/parameters cannot be parsed / are invalid
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"401":
|
||||
description: user is not authorized / has missing permissions
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"404":
|
||||
description: channel not found
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
summary: Get a single channel (similar to api-channels-get, but can be called
|
||||
from anyone and only returns a subset of fields)
|
||||
tags:
|
||||
- API-v2
|
||||
/api/v2/preview/keys/{kid}:
|
||||
get:
|
||||
operationId: api-tokenkeys-get-preview
|
||||
parameters:
|
||||
- description: TokenKeyID
|
||||
in: path
|
||||
name: kid
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.KeyTokenPreviewJSON'
|
||||
"400":
|
||||
description: supplied values/parameters cannot be parsed / are invalid
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"401":
|
||||
description: user is not authorized / has missing permissions
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"404":
|
||||
description: message not found
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
summary: Get a single key (similar to api-tokenkeys-get, but can be called from
|
||||
anyone and only returns a subset of fields)
|
||||
tags:
|
||||
- API-v2
|
||||
/api/v2/preview/users/{uid}:
|
||||
get:
|
||||
operationId: api-user-get-preview
|
||||
parameters:
|
||||
- description: UserID
|
||||
in: path
|
||||
name: uid
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.UserPreviewJSON'
|
||||
"400":
|
||||
description: supplied values/parameters cannot be parsed / are invalid
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"401":
|
||||
description: user is not authorized / has missing permissions
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"404":
|
||||
description: user not found
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
summary: Get a user (similar to api-user-get, but can be called from anyone
|
||||
and only returns a subset of fields)
|
||||
tags:
|
||||
- API-v2
|
||||
/api/v2/users:
|
||||
post:
|
||||
operationId: api-user-create
|
||||
@ -2491,52 +2685,90 @@ paths:
|
||||
description: All parameter can be set via query-parameter or the json body.
|
||||
Only UserID, UserKey and Title are required
|
||||
parameters:
|
||||
- in: query
|
||||
- example: test
|
||||
in: query
|
||||
name: channel
|
||||
type: string
|
||||
- example: This is a message
|
||||
in: query
|
||||
name: content
|
||||
type: string
|
||||
- in: query
|
||||
- example: P3TNH8mvv14fm
|
||||
in: query
|
||||
name: key
|
||||
type: string
|
||||
- example: db8b0e6a-a08c-4646
|
||||
in: query
|
||||
name: msg_id
|
||||
type: string
|
||||
- in: query
|
||||
- enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
example: 1
|
||||
in: query
|
||||
name: priority
|
||||
type: integer
|
||||
- in: query
|
||||
- example: example-server
|
||||
in: query
|
||||
name: sender_name
|
||||
type: string
|
||||
- example: 1669824037
|
||||
in: query
|
||||
name: timestamp
|
||||
type: number
|
||||
- in: query
|
||||
- example: Hello World
|
||||
in: query
|
||||
name: title
|
||||
type: string
|
||||
- in: query
|
||||
- example: "7725"
|
||||
in: query
|
||||
name: user_id
|
||||
type: integer
|
||||
- in: query
|
||||
name: user_key
|
||||
type: string
|
||||
- description: ' '
|
||||
in: body
|
||||
name: post_body
|
||||
schema:
|
||||
$ref: '#/definitions/handler.SendMessage.combined'
|
||||
- in: formData
|
||||
- example: test
|
||||
in: formData
|
||||
name: channel
|
||||
type: string
|
||||
- example: This is a message
|
||||
in: formData
|
||||
name: content
|
||||
type: string
|
||||
- in: formData
|
||||
- example: P3TNH8mvv14fm
|
||||
in: formData
|
||||
name: key
|
||||
type: string
|
||||
- example: db8b0e6a-a08c-4646
|
||||
in: formData
|
||||
name: msg_id
|
||||
type: string
|
||||
- in: formData
|
||||
- enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
example: 1
|
||||
in: formData
|
||||
name: priority
|
||||
type: integer
|
||||
- in: formData
|
||||
- example: example-server
|
||||
in: formData
|
||||
name: sender_name
|
||||
type: string
|
||||
- example: 1669824037
|
||||
in: formData
|
||||
name: timestamp
|
||||
type: number
|
||||
- in: formData
|
||||
- example: Hello World
|
||||
in: formData
|
||||
name: title
|
||||
type: string
|
||||
- in: formData
|
||||
- example: "7725"
|
||||
in: formData
|
||||
name: user_id
|
||||
type: integer
|
||||
- in: formData
|
||||
name: user_key
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
@ -2569,47 +2801,85 @@ paths:
|
||||
description: All parameter can be set via query-parameter or form-data body.
|
||||
Only UserID, UserKey and Title are required
|
||||
parameters:
|
||||
- in: query
|
||||
- example: test
|
||||
in: query
|
||||
name: channel
|
||||
type: string
|
||||
- example: This is a message
|
||||
in: query
|
||||
name: content
|
||||
type: string
|
||||
- in: query
|
||||
- example: P3TNH8mvv14fm
|
||||
in: query
|
||||
name: key
|
||||
type: string
|
||||
- example: db8b0e6a-a08c-4646
|
||||
in: query
|
||||
name: msg_id
|
||||
type: string
|
||||
- in: query
|
||||
- enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
example: 1
|
||||
in: query
|
||||
name: priority
|
||||
type: integer
|
||||
- in: query
|
||||
- example: example-server
|
||||
in: query
|
||||
name: sender_name
|
||||
type: string
|
||||
- example: 1669824037
|
||||
in: query
|
||||
name: timestamp
|
||||
type: number
|
||||
- in: query
|
||||
- example: Hello World
|
||||
in: query
|
||||
name: title
|
||||
type: string
|
||||
- in: query
|
||||
- example: "7725"
|
||||
in: query
|
||||
name: user_id
|
||||
type: integer
|
||||
- in: query
|
||||
name: user_key
|
||||
type: string
|
||||
- in: formData
|
||||
- example: test
|
||||
in: formData
|
||||
name: channel
|
||||
type: string
|
||||
- example: This is a message
|
||||
in: formData
|
||||
name: content
|
||||
type: string
|
||||
- in: formData
|
||||
- example: P3TNH8mvv14fm
|
||||
in: formData
|
||||
name: key
|
||||
type: string
|
||||
- example: db8b0e6a-a08c-4646
|
||||
in: formData
|
||||
name: msg_id
|
||||
type: string
|
||||
- in: formData
|
||||
- enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
example: 1
|
||||
in: formData
|
||||
name: priority
|
||||
type: integer
|
||||
- in: formData
|
||||
- example: example-server
|
||||
in: formData
|
||||
name: sender_name
|
||||
type: string
|
||||
- example: 1669824037
|
||||
in: formData
|
||||
name: timestamp
|
||||
type: number
|
||||
- in: formData
|
||||
- example: Hello World
|
||||
in: formData
|
||||
name: title
|
||||
type: string
|
||||
- in: formData
|
||||
- example: "7725"
|
||||
in: formData
|
||||
name: user_id
|
||||
type: integer
|
||||
- in: formData
|
||||
name: user_key
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
|
Loading…
Reference in New Issue
Block a user