⬆️ Upgrade to support latest version of server

This commit is contained in:
LittleSheep 2024-07-16 19:46:53 +08:00
parent 286dd8193d
commit da265da61d
42 changed files with 221 additions and 297 deletions

View File

@ -8,9 +8,8 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/firebase_options.dart'; import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/account.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/chat.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
import 'package:solian/providers/content/call.dart'; import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
@ -100,8 +99,7 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => FriendProvider()); Get.lazyPut(() => FriendProvider());
Get.lazyPut(() => FeedProvider()); Get.lazyPut(() => FeedProvider());
Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => ChatProvider()); Get.lazyPut(() => WebSocketProvider());
Get.lazyPut(() => AccountProvider());
Get.lazyPut(() => StatusProvider()); Get.lazyPut(() => StatusProvider());
Get.lazyPut(() => ChannelProvider()); Get.lazyPut(() => ChannelProvider());
Get.lazyPut(() => RealmProvider()); Get.lazyPut(() => RealmProvider());
@ -109,13 +107,12 @@ class SolianApp extends StatelessWidget {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (await auth.isAuthorized) { if (await auth.isAuthorized) {
Get.find<AccountProvider>().connect(); Get.find<WebSocketProvider>().connect();
Get.find<ChatProvider>().connect();
Get.find<ChannelProvider>().refreshAvailableChannel(); Get.find<ChannelProvider>().refreshAvailableChannel();
try { try {
Get.find<AccountProvider>().registerPushNotifications(); Get.find<WebSocketProvider>().registerPushNotifications();
} catch (err) { } catch (err) {
context.showSnackbar( context.showSnackbar(
'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}), 'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}),

View File

@ -1,5 +1,6 @@
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/feed.dart'; import 'package:solian/models/feed.dart';
import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
class Article { class Article {
@ -20,8 +21,7 @@ class Article {
bool? isDraft; bool? isDraft;
int authorId; int authorId;
Account author; Account author;
int reactionCount; PostMetric? metric;
Map<String, int> reactionList;
Article({ Article({
required this.id, required this.id,
@ -41,8 +41,7 @@ class Article {
required this.isDraft, required this.isDraft,
required this.authorId, required this.authorId,
required this.author, required this.author,
required this.reactionCount, required this.metric,
required this.reactionList,
}); });
factory Article.fromJson(Map<String, dynamic> json) => Article( factory Article.fromJson(Map<String, dynamic> json) => Article(
@ -72,15 +71,8 @@ class Article {
isDraft: json['is_draft'], isDraft: json['is_draft'],
authorId: json['author_id'], authorId: json['author_id'],
author: Account.fromJson(json['author']), author: Account.fromJson(json['author']),
reactionCount: json['reaction_count'], metric:
reactionList: json['reaction_list'] != null json['metric'] != null ? PostMetric.fromJson(json['metric']) : null,
? json['reaction_list']
.map((key, value) => MapEntry(
key,
int.tryParse(value.toString()) ??
(value is double ? value.toInt() : null)))
.cast<String, int>()
: {},
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@ -101,7 +93,6 @@ class Article {
'is_draft': isDraft, 'is_draft': isDraft,
'author_id': authorId, 'author_id': authorId,
'author': author.toJson(), 'author': author.toJson(),
'reaction_count': reactionCount, 'metric': metric?.toJson(),
'reaction_list': reactionList,
}; };
} }

View File

@ -23,9 +23,7 @@ class Post {
bool? isDraft; bool? isDraft;
int authorId; int authorId;
Account author; Account author;
int replyCount; PostMetric? metric;
int reactionCount;
Map<String, int> reactionList;
Post({ Post({
required this.id, required this.id,
@ -48,9 +46,7 @@ class Post {
required this.isDraft, required this.isDraft,
required this.authorId, required this.authorId,
required this.author, required this.author,
required this.replyCount, required this.metric,
required this.reactionCount,
required this.reactionList,
}); });
factory Post.fromJson(Map<String, dynamic> json) => Post( factory Post.fromJson(Map<String, dynamic> json) => Post(
@ -85,16 +81,8 @@ class Post {
isDraft: json['is_draft'], isDraft: json['is_draft'],
authorId: json['author_id'], authorId: json['author_id'],
author: Account.fromJson(json['author']), author: Account.fromJson(json['author']),
replyCount: json['reply_count'], metric:
reactionCount: json['reaction_count'], json['metric'] != null ? PostMetric.fromJson(json['metric']) : null,
reactionList: json['reaction_list'] != null
? json['reaction_list']
.map((key, value) => MapEntry(
key,
int.tryParse(value.toString()) ??
(value is double ? value.toInt() : null)))
.cast<String, int>()
: {},
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@ -118,8 +106,37 @@ class Post {
'is_draft': isDraft, 'is_draft': isDraft,
'author_id': authorId, 'author_id': authorId,
'author': author.toJson(), 'author': author.toJson(),
'reply_count': replyCount, 'metric': metric?.toJson(),
};
}
class PostMetric {
int reactionCount;
Map<String, int> reactionList;
int replyCount;
PostMetric({
required this.reactionCount,
required this.reactionList,
required this.replyCount,
});
factory PostMetric.fromJson(Map<String, dynamic> json) => PostMetric(
reactionCount: json['reaction_count'],
replyCount: json['reply_count'],
reactionList: json['reaction_list'] != null
? json['reaction_list']
.map((key, value) => MapEntry(
key,
int.tryParse(value.toString()) ??
(value is double ? value.toInt() : null)))
.cast<String, int>()
: {},
);
Map<String, dynamic> toJson() => {
'reaction_count': reactionCount, 'reaction_count': reactionCount,
'reply_count': replyCount,
'reaction_list': reactionList, 'reaction_list': reactionList,
}; };
} }

View File

@ -27,7 +27,7 @@ class StatusProvider extends GetConnect {
void onInit() { void onInit() {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
httpClient.baseUrl = ServiceFinder.services['passport']; httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
httpClient.addAuthenticator(auth.requestAuthenticator); httpClient.addAuthenticator(auth.requestAuthenticator);
} }
@ -35,13 +35,13 @@ class StatusProvider extends GetConnect {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized'); if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
return await client.get('/api/users/me/status'); return await client.get('/users/me/status');
} }
Future<Response> getSomeoneStatus(String name) => Future<Response> getSomeoneStatus(String name) =>
get('/api/users/$name/status'); get('/users/$name/status');
Future<Response> setStatus( Future<Response> setStatus(
String type, String type,
@ -55,7 +55,7 @@ class StatusProvider extends GetConnect {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized'); if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final payload = { final payload = {
'type': type, 'type': type,
@ -68,9 +68,9 @@ class StatusProvider extends GetConnect {
Response resp; Response resp;
if (!isUpdate) { if (!isUpdate) {
resp = await client.post('/api/users/me/status', payload); resp = await client.post('/users/me/status', payload);
} else { } else {
resp = await client.put('/api/users/me/status', payload); resp = await client.put('/users/me/status', payload);
} }
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
@ -84,9 +84,9 @@ class StatusProvider extends GetConnect {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized'); if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.delete('/api/users/me/status'); final resp = await client.delete('/users/me/status');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }

View File

@ -8,8 +8,7 @@ import 'package:get/get.dart';
import 'package:get/get_connect/http/src/request/request.dart'; import 'package:get/get_connect/http/src/request/request.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:solian/controllers/chat_events_controller.dart'; import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/providers/account.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/chat.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
class TokenSet { class TokenSet {
@ -48,7 +47,7 @@ class RiskyAuthenticateException implements Exception {
class AuthProvider extends GetConnect { class AuthProvider extends GetConnect {
final tokenEndpoint = final tokenEndpoint =
Uri.parse('${ServiceFinder.services['passport']}/api/auth/token'); Uri.parse(ServiceFinder.buildUrl('auth', '/auth/token'));
static const clientId = 'solian'; static const clientId = 'solian';
static const clientSecret = '_F4%q2Eea3'; static const clientSecret = '_F4%q2Eea3';
@ -60,7 +59,7 @@ class AuthProvider extends GetConnect {
@override @override
void onInit() { void onInit() {
httpClient.baseUrl = ServiceFinder.services['passport']; httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
loadCredentials(); loadCredentials();
} }
@ -68,7 +67,7 @@ class AuthProvider extends GetConnect {
try { try {
credentialsRefreshMutex.acquire(); credentialsRefreshMutex.acquire();
if (!credentials!.isExpired) return; if (!credentials!.isExpired) return;
final resp = await post('/api/auth/token', { final resp = await post('/auth/token', {
'refresh_token': credentials!.refreshToken, 'refresh_token': credentials!.refreshToken,
'grant_type': 'refresh_token', 'grant_type': 'refresh_token',
}); });
@ -111,7 +110,7 @@ class AuthProvider extends GetConnect {
sendUserAgent: true, sendUserAgent: true,
); );
client.httpClient.addAuthenticator(requestAuthenticator); client.httpClient.addAuthenticator(requestAuthenticator);
client.httpClient.baseUrl = ServiceFinder.services[service]; client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
return client; return client;
} }
@ -140,10 +139,10 @@ class AuthProvider extends GetConnect {
) async { ) async {
_cachedUserProfileResponse = null; _cachedUserProfileResponse = null;
final client = ServiceFinder.configureClient('passport'); final client = ServiceFinder.configureClient('auth');
// Create ticket // Create ticket
final resp = await client.post('/api/auth', { final resp = await client.post('/auth', {
'username': username, 'username': username,
'password': password, 'password': password,
}); });
@ -154,7 +153,7 @@ class AuthProvider extends GetConnect {
} }
// Assign token // Assign token
final tokenResp = await post('/api/auth/token', { final tokenResp = await post('/auth/token', {
'code': resp.body['ticket']['grant_token'], 'code': resp.body['ticket']['grant_token'],
'grant_type': 'grant_token', 'grant_type': 'grant_token',
}); });
@ -173,9 +172,8 @@ class AuthProvider extends GetConnect {
value: jsonEncode(credentials!.toJson()), value: jsonEncode(credentials!.toJson()),
); );
Get.find<AccountProvider>().connect(); Get.find<WebSocketProvider>().connect();
Get.find<AccountProvider>().notifyPrefetch(); Get.find<WebSocketProvider>().notifyPrefetch();
Get.find<ChatProvider>().connect();
return credentials!; return credentials!;
} }
@ -183,10 +181,9 @@ class AuthProvider extends GetConnect {
void signout() { void signout() {
_cachedUserProfileResponse = null; _cachedUserProfileResponse = null;
Get.find<ChatProvider>().disconnect(); Get.find<WebSocketProvider>().disconnect();
Get.find<AccountProvider>().disconnect(); Get.find<WebSocketProvider>().notifications.clear();
Get.find<AccountProvider>().notifications.clear(); Get.find<WebSocketProvider>().notificationUnread.value = 0;
Get.find<AccountProvider>().notificationUnread.value = 0;
final chatHistory = ChatEventController(); final chatHistory = ChatEventController();
chatHistory.initialize().then((_) async { chatHistory.initialize().then((_) async {
@ -207,9 +204,9 @@ class AuthProvider extends GetConnect {
return _cachedUserProfileResponse!; return _cachedUserProfileResponse!;
} }
final client = configureClient('passport'); final client = configureClient('auth');
final resp = await client.get('/api/users/me'); final resp = await client.get('/users/me');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} else { } else {

View File

@ -1,76 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:get/get.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class ChatProvider extends GetxController {
RxBool isConnected = false.obs;
RxBool isConnecting = false.obs;
WebSocketChannel? websocket;
StreamController<NetworkPackage> stream = StreamController.broadcast();
void connect({noRetry = false}) async {
if (isConnected.value) {
return;
} else {
disconnect();
}
final AuthProvider auth = Get.find();
await auth.ensureCredentials();
if (auth.credentials == null) await auth.loadCredentials();
final uri = Uri.parse(
'${ServiceFinder.services['messaging']}/api/ws?tk=${auth.credentials!.accessToken}'
.replaceFirst('http', 'ws'),
);
isConnecting.value = true;
try {
websocket = WebSocketChannel.connect(uri);
await websocket?.ready;
} catch (e) {
if (!noRetry) {
await auth.refreshCredentials();
return connect(noRetry: true);
}
}
listen();
isConnected.value = true;
isConnecting.value = false;
}
void disconnect() {
websocket?.sink.close(WebSocketStatus.normalClosure);
websocket = null;
isConnected.value = false;
}
void listen() {
websocket?.stream.listen(
(event) {
final packet = NetworkPackage.fromJson(jsonDecode(event));
stream.sink.add(packet);
},
onDone: () {
isConnected.value = false;
Future.delayed(const Duration(seconds: 1), () => connect());
},
onError: (err) {
isConnected.value = false;
Future.delayed(const Duration(seconds: 3), () => connect());
},
);
}
}

View File

@ -58,7 +58,7 @@ class AttachmentProvider extends GetConnect {
@override @override
void onInit() { void onInit() {
httpClient.baseUrl = ServiceFinder.services['paperclip']; httpClient.baseUrl = ServiceFinder.buildUrl('files', null);
} }
final Map<int, Response> _cachedResponses = {}; final Map<int, Response> _cachedResponses = {};
@ -68,7 +68,7 @@ class AttachmentProvider extends GetConnect {
return _cachedResponses[id]!; return _cachedResponses[id]!;
} }
final resp = await get('/api/attachments/$id/meta'); final resp = await get('/attachments/$id/meta');
_cachedResponses[id] = resp; _cachedResponses[id] = resp;
return resp; return resp;
@ -81,7 +81,7 @@ class AttachmentProvider extends GetConnect {
if (!await auth.isAuthorized) throw Exception('unauthorized'); if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient( final client = auth.configureClient(
'paperclip', 'files',
timeout: const Duration(minutes: 3), timeout: const Duration(minutes: 3),
); );
@ -108,7 +108,7 @@ class AttachmentProvider extends GetConnect {
if (ratio != null) 'ratio': ratio, if (ratio != null) 'ratio': ratio,
}), }),
}); });
final resp = await client.post('/api/attachments', payload); final resp = await client.post('/attachments', payload);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }
@ -126,9 +126,9 @@ class AttachmentProvider extends GetConnect {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized'); if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient('paperclip'); final client = auth.configureClient('files');
var resp = await client.put('/api/attachments/$id', { var resp = await client.put('/attachments/$id', {
'metadata': { 'metadata': {
if (ratio != null) 'ratio': ratio, if (ratio != null) 'ratio': ratio,
}, },
@ -148,9 +148,9 @@ class AttachmentProvider extends GetConnect {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized'); if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient('paperclip'); final client = auth.configureClient('files');
var resp = await client.delete('/api/attachments/$id'); var resp = await client.delete('/attachments/$id');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }

View File

@ -60,7 +60,7 @@ class ChatCallProvider extends GetxController {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.post( final resp = await client.post(
'/api/channels/global/${channel.value!.alias}/calls/ongoing/token', '/channels/global/${channel.value!.alias}/calls/ongoing/token',
{}, {},
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {

View File

@ -33,7 +33,7 @@ class ChannelProvider extends GetxController {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$realm/$alias'); final resp = await client.get('/channels/$realm/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }
@ -48,7 +48,7 @@ class ChannelProvider extends GetxController {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$realm/$alias/me'); final resp = await client.get('/channels/$realm/$alias/me');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }
@ -63,7 +63,7 @@ class ChannelProvider extends GetxController {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$realm/$alias/calls/ongoing'); final resp = await client.get('/channels/$realm/$alias/calls/ongoing');
if (resp.statusCode == 404) { if (resp.statusCode == 404) {
return null; return null;
} else if (resp.statusCode != 200) { } else if (resp.statusCode != 200) {
@ -79,7 +79,7 @@ class ChannelProvider extends GetxController {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$scope'); final resp = await client.get('/channels/$scope');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }
@ -93,7 +93,7 @@ class ChannelProvider extends GetxController {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$realm/me/available'); final resp = await client.get('/channels/$realm/me/available');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }
@ -107,7 +107,7 @@ class ChannelProvider extends GetxController {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.post('/api/channels/$scope', payload); final resp = await client.post('/channels/$scope', payload);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }
@ -132,7 +132,7 @@ class ChannelProvider extends GetxController {
final prof = await auth.getProfile(); final prof = await auth.getProfile();
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.post('/api/channels/$scope/dm', { final resp = await client.post('/channels/$scope/dm', {
'alias': const Uuid().v4().replaceAll('-', '').substring(0, 12), 'alias': const Uuid().v4().replaceAll('-', '').substring(0, 12),
'name': 'DM', 'name': 'DM',
'description': 'description':
@ -153,7 +153,7 @@ class ChannelProvider extends GetxController {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.put('/api/channels/$scope/$id', payload); final resp = await client.put('/channels/$scope/$id', payload);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }

View File

@ -5,7 +5,7 @@ import 'package:solian/services.dart';
class FeedProvider extends GetConnect { class FeedProvider extends GetConnect {
@override @override
void onInit() { void onInit() {
httpClient.baseUrl = ServiceFinder.services['interactive']; httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
} }
Future<Response> listFeed(int page, Future<Response> listFeed(int page,
@ -17,7 +17,7 @@ class FeedProvider extends GetConnect {
if (category != null) 'category=$category', if (category != null) 'category=$category',
if (realm != null) 'realmId=$realm', if (realm != null) 'realmId=$realm',
]; ];
final resp = await get('/api/feed?${queries.join('&')}'); final resp = await get('/feed?${queries.join('&')}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.body); throw Exception(resp.body);
} }
@ -34,7 +34,7 @@ class FeedProvider extends GetConnect {
'offset=$page', 'offset=$page',
]; ];
final client = auth.configureClient('interactive'); final client = auth.configureClient('interactive');
final resp = await client.get('/api/drafts?${queries.join('&')}'); final resp = await client.get('/drafts?${queries.join('&')}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.body); throw Exception(resp.body);
} }
@ -48,7 +48,7 @@ class FeedProvider extends GetConnect {
'offset=$page', 'offset=$page',
if (realm != null) 'realmId=$realm', if (realm != null) 'realmId=$realm',
]; ];
final resp = await get('/api/posts?${queries.join('&')}'); final resp = await get('/posts?${queries.join('&')}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.body); throw Exception(resp.body);
} }
@ -57,7 +57,7 @@ class FeedProvider extends GetConnect {
} }
Future<Response> listPostReplies(String alias, int page) async { Future<Response> listPostReplies(String alias, int page) async {
final resp = await get('/api/posts/$alias/replies?take=${10}&offset=$page'); final resp = await get('/posts/$alias/replies?take=${10}&offset=$page');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.body); throw Exception(resp.body);
} }
@ -66,7 +66,7 @@ class FeedProvider extends GetConnect {
} }
Future<Response> getPost(String alias) async { Future<Response> getPost(String alias) async {
final resp = await get('/api/posts/$alias'); final resp = await get('/posts/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.body); throw Exception(resp.body);
} }
@ -75,7 +75,7 @@ class FeedProvider extends GetConnect {
} }
Future<Response> getArticle(String alias) async { Future<Response> getArticle(String alias) async {
final resp = await get('/api/articles/$alias'); final resp = await get('/articles/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.body); throw Exception(resp.body);
} }

View File

@ -6,9 +6,9 @@ class RealmProvider extends GetxController {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized'); if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.get('/api/realms/$alias'); final resp = await client.get('/realms/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }
@ -20,9 +20,9 @@ class RealmProvider extends GetxController {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized'); if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.get('/api/realms/me/available'); final resp = await client.get('/realms/me/available');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }

View File

@ -8,17 +8,17 @@ class FriendProvider extends GetConnect {
void onInit() { void onInit() {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
httpClient.baseUrl = ServiceFinder.services['passport']; httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
httpClient.addAuthenticator(auth.requestAuthenticator); httpClient.addAuthenticator(auth.requestAuthenticator);
} }
Future<Response> listFriendship() => get('/api/users/me/friends'); Future<Response> listFriendship() => get('/users/me/friends');
Future<Response> listFriendshipWithStatus(int status) => Future<Response> listFriendshipWithStatus(int status) =>
get('/api/users/me/friends?status=$status'); get('/users/me/friends?status=$status');
Future<Response> createFriendship(String username) async { Future<Response> createFriendship(String username) async {
final resp = await post('/api/users/me/friends?related=$username', {}); final resp = await post('/users/me/friends?related=$username', {});
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }
@ -31,7 +31,7 @@ class FriendProvider extends GetConnect {
final prof = await auth.getProfile(); final prof = await auth.getProfile();
final otherside = relationship.getOtherside(prof.body['id']); final otherside = relationship.getOtherside(prof.body['id']);
final resp = await put('/api/users/me/friends/${otherside.id}', { final resp = await put('/users/me/friends/${otherside.id}', {
'status': status, 'status': status,
}); });
if (resp.statusCode != 200) { if (resp.statusCode != 200) {

View File

@ -23,7 +23,7 @@ Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.get( final resp = await client.get(
'/api/channels/$scope/${channel.alias}/events/$id', '/channels/$scope/${channel.alias}/events/$id',
); );
if (resp.statusCode == 404) { if (resp.statusCode == 404) {
@ -53,7 +53,7 @@ Future<(List<Event>, int)?> getRemoteEvents(
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.get( final resp = await client.get(
'/api/channels/$scope/${channel.alias}/events?take=$take&offset=$offset', '/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
); );
if (resp.statusCode != 200) { if (resp.statusCode != 200) {

View File

@ -16,7 +16,7 @@ import 'package:solian/services.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class AccountProvider extends GetxController { class WebSocketProvider extends GetxController {
final FlutterLocalNotificationsPlugin localNotify = final FlutterLocalNotificationsPlugin localNotify =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
@ -29,6 +29,8 @@ class AccountProvider extends GetxController {
WebSocketChannel? websocket; WebSocketChannel? websocket;
StreamController<NetworkPackage> stream = StreamController.broadcast();
@override @override
onInit() { onInit() {
FirebaseMessaging.instance FirebaseMessaging.instance
@ -58,10 +60,10 @@ class AccountProvider extends GetxController {
if (auth.credentials == null) await auth.loadCredentials(); if (auth.credentials == null) await auth.loadCredentials();
final uri = Uri.parse( final uri = Uri.parse(ServiceFinder.buildUrl(
'${ServiceFinder.services['passport']}/api/ws?tk=${auth.credentials!.accessToken}' 'dealer',
.replaceFirst('http', 'ws'), '/api/ws?tk=${auth.credentials!.accessToken}',
); ).replaceFirst('http', 'ws'));
isConnecting.value = true; isConnecting.value = true;
@ -91,6 +93,8 @@ class AccountProvider extends GetxController {
websocket?.stream.listen( websocket?.stream.listen(
(event) { (event) {
final packet = NetworkPackage.fromJson(jsonDecode(event)); final packet = NetworkPackage.fromJson(jsonDecode(event));
stream.sink.add(packet);
switch (packet.method) { switch (packet.method) {
case 'notifications.new': case 'notifications.new':
final notification = Notification.fromJson(packet.payload!); final notification = Notification.fromJson(packet.payload!);
@ -166,9 +170,9 @@ class AccountProvider extends GetxController {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return; if (!await auth.isAuthorized) return;
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.get('/api/notifications?skip=0&take=100'); final resp = await client.get('/notifications?skip=0&take=100');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body); final result = PaginationResult.fromJson(resp.body);
final data = result.data?.map((x) => Notification.fromJson(x)).toList(); final data = result.data?.map((x) => Notification.fromJson(x)).toList();
@ -199,9 +203,9 @@ class AccountProvider extends GetxController {
token = await FirebaseMessaging.instance.getToken(); token = await FirebaseMessaging.instance.getToken();
} }
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.post('/api/notifications/subscribe', { final resp = await client.post('/notifications/subscribe', {
'provider': provider, 'provider': provider,
'device_token': token, 'device_token': token,
'device_id': deviceUuid, 'device_id': deviceUuid,

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/providers/account.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/models/notification.dart' as notify; import 'package:solian/models/notification.dart' as notify;
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -23,7 +23,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final AccountProvider provider = Get.find(); final WebSocketProvider provider = Get.find();
List<int> markList = List.empty(growable: true); List<int> markList = List.empty(growable: true);
for (final element in provider.notifications) { for (final element in provider.notifications) {
@ -32,8 +32,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
} }
if (markList.isNotEmpty) { if (markList.isNotEmpty) {
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
await client.put('/api/notifications/batch/read', {'messages': markList}); await client.put('/notifications/batch/read', {'messages': markList});
} }
provider.notifications.clear(); provider.notifications.clear();
@ -45,7 +45,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return; if (!await auth.isAuthorized) return;
final AccountProvider provider = Get.find(); final WebSocketProvider provider = Get.find();
if (element.id <= 0) { if (element.id <= 0) {
provider.notifications.removeAt(index); provider.notifications.removeAt(index);
@ -54,9 +54,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
await client.put('/api/notifications/${element.id}/read', {}); await client.put('/notifications/${element.id}/read', {});
provider.notifications.removeAt(index); provider.notifications.removeAt(index);
@ -65,7 +65,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AccountProvider provider = Get.find(); final WebSocketProvider provider = Get.find();
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height * 0.85, height: MediaQuery.of(context).size.height * 0.85,
@ -175,7 +175,7 @@ class NotificationButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AccountProvider provider = Get.find(); final WebSocketProvider provider = Get.find();
final button = IconButton( final button = IconButton(
icon: const Icon(Icons.notifications), icon: const Icon(Icons.notifications),

View File

@ -100,10 +100,10 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
return; return;
} }
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.put( final resp = await client.put(
'/api/users/me/$position', '/users/me/$position',
{'attachment': attachResp.body['id']}, {'attachment': attachResp.body['id']},
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
@ -122,11 +122,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
_birthday?.toIso8601String(); _birthday?.toIso8601String();
final resp = await client.put( final resp = await client.put(
'/api/users/me', '/users/me',
{ {
'nick': _nicknameController.value.text, 'nick': _nicknameController.value.text,
'description': _descriptionController.value.text, 'description': _descriptionController.value.text,
@ -189,7 +189,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: _banner != null child: _banner != null
? Image.network( ? Image.network(
'${ServiceFinder.services['paperclip']}/api/attachments/$_banner', ServiceFinder.buildUrl('files', '/attachments/$_banner'),
fit: BoxFit.cover, fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child, loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) { ImageChunkEvent? loadingProgress) {

View File

@ -84,9 +84,9 @@ class _ArticlePublishScreenState extends State<ArticlePublishScreen> {
Response resp; Response resp;
if (widget.edit != null) { if (widget.edit != null) {
resp = await client.put('/api/articles/${widget.edit!.id}', payload); resp = await client.put('/articles/${widget.edit!.id}', payload);
} else { } else {
resp = await client.post('/api/articles', payload); resp = await client.post('/articles', payload);
} }
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:protocol_handler/protocol_handler.dart'; import 'package:protocol_handler/protocol_handler.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/providers/account.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -30,15 +30,15 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = ServiceFinder.configureClient('passport'); final client = ServiceFinder.configureClient('auth');
final lookupResp = await client.get('/api/users/lookup?probe=$username'); final lookupResp = await client.get('/users/lookup?probe=$username');
if (lookupResp.statusCode != 200) { if (lookupResp.statusCode != 200) {
context.showErrorDialog(lookupResp.bodyString); context.showErrorDialog(lookupResp.bodyString);
setState(() => _isBusy = false); setState(() => _isBusy = false);
return; return;
} }
final resp = await client.post('/api/users/me/password-reset', { final resp = await client.post('/users/me/password-reset', {
'user_id': lookupResp.body['id'], 'user_id': lookupResp.body['id'],
}); });
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
@ -75,7 +75,7 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
onPressed: () { onPressed: () {
const redirect = 'solink://auth?status=done'; const redirect = 'solink://auth?status=done';
launchUrlString( launchUrlString(
'${ServiceFinder.services['passport']}/mfa?redirect_uri=$redirect&ticketId=${e.ticketId}', ServiceFinder.buildUrl('passport', '/mfa?redirect_uri=$redirect&ticketId=${e.ticketId}'),
mode: LaunchMode.inAppWebView, mode: LaunchMode.inAppWebView,
); );
Navigator.pop(context); Navigator.pop(context);
@ -93,7 +93,7 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
Get.find<AccountProvider>().registerPushNotifications(); Get.find<WebSocketProvider>().registerPushNotifications();
Navigator.pop(context, true); Navigator.pop(context, true);
} }

View File

@ -26,9 +26,9 @@ class _SignUpPopupState extends State<SignUpPopup> {
nickname.isEmpty || nickname.isEmpty ||
password.isEmpty) return; password.isEmpty) return;
final client = ServiceFinder.configureClient('passport'); final client = ServiceFinder.configureClient('auth');
final resp = await client.post('/api/users', { final resp = await client.post('/users', {
'name': username, 'name': username,
'nick': nickname, 'nick': nickname,
'email': email, 'email': email,

View File

@ -11,9 +11,9 @@ import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart'; import 'package:solian/models/event.dart';
import 'package:solian/models/packet.dart'; import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/chat.dart';
import 'package:solian/providers/content/call.dart'; import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/channel/channel_detail.dart'; import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@ -107,7 +107,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
} }
void listenMessages() { void listenMessages() {
final ChatProvider provider = Get.find(); final WebSocketProvider provider = Get.find();
_subscription = provider.stream.stream.listen((event) { _subscription = provider.stream.stream.listen((event) {
switch (event.method) { switch (event.method) {
case 'events.new': case 'events.new':

View File

@ -82,7 +82,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.put( final resp = await client.put(
'/api/channels/${widget.realm}/${widget.channel.alias}/members/me', { '/channels/${widget.realm}/${widget.channel.alias}/members/me', {
'nick': null, 'nick': null,
'notify_level': _notifyLevel, 'notify_level': _notifyLevel,
}); });

View File

@ -90,9 +90,9 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
Response resp; Response resp;
if (widget.edit != null) { if (widget.edit != null) {
resp = await client.put('/api/posts/${widget.edit!.id}', payload); resp = await client.put('/posts/${widget.edit!.id}', payload);
} else { } else {
resp = await client.post('/api/posts', payload); resp = await client.post('/posts', payload);
} }
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);

View File

@ -43,7 +43,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final payload = { final payload = {
'alias': _aliasController.value.text.toLowerCase(), 'alias': _aliasController.value.text.toLowerCase(),
@ -55,9 +55,9 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
Response resp; Response resp;
if (widget.edit != null) { if (widget.edit != null) {
resp = await client.put('/api/realms/${widget.edit!.id}', payload); resp = await client.put('/realms/${widget.edit!.id}', payload);
} else { } else {
resp = await client.post('/api/realms', payload); resp = await client.post('/realms', payload);
} }
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);

View File

@ -3,23 +3,29 @@ import 'package:get/get.dart';
abstract class ServiceFinder { abstract class ServiceFinder {
static const bool devFlag = false; static const bool devFlag = false;
static Map<String, String> services = { static const String dealerUrl =
'paperclip': devFlag ? 'http://localhost:8442' : 'https://api.sn.solsynth.dev';
devFlag ? 'http://localhost:8443' : 'https://usercontent.solsynth.dev', static const String passportUrl =
'passport': devFlag ? 'http://localhost:8444' : 'https://id.solsynth.dev', devFlag ? 'http://localhost:8444' : 'https://id.solsynth.dev';
'interactive':
devFlag ? 'http://localhost:8445' : 'https://co.solsynth.dev',
'messaging': devFlag ? 'http://localhost:8447' : 'https://im.solsynth.dev',
};
static GetConnect configureClient(String service, static String buildUrl(String serviceName, String? append) {
append ??= '';
if (serviceName == 'dealer') {
return '$dealerUrl$append';
} else if (serviceName == 'passport') {
return '$passportUrl$append';
}
return '$dealerUrl/srv/$serviceName$append';
}
static GetConnect configureClient(String serviceName,
{timeout = const Duration(seconds: 5)}) { {timeout = const Duration(seconds: 5)}) {
final client = GetConnect( final client = GetConnect(
timeout: timeout, timeout: timeout,
userAgent: 'Solian/1.1', userAgent: 'Solian/1.1',
sendUserAgent: true, sendUserAgent: true,
); );
client.httpClient.baseUrl = ServiceFinder.services[service]; client.httpClient.baseUrl = buildUrl(serviceName, null);
return client; return client;
} }

View File

@ -24,12 +24,12 @@ class AccountAvatar extends StatelessWidget {
if (content is String) { if (content is String) {
direct = content.startsWith('http'); direct = content.startsWith('http');
if (!isEmpty) isEmpty = content.isEmpty; if (!isEmpty) isEmpty = content.isEmpty;
if (!isEmpty) isEmpty = content.endsWith('/api/attachments/0'); if (!isEmpty) isEmpty = content.endsWith('/attachments/0');
} }
final url = direct final url = direct
? content ? content
: '${ServiceFinder.services['paperclip']}/api/attachments/$content'; : ServiceFinder.buildUrl('files', '/attachments/$content');
return CircleAvatar( return CircleAvatar(
key: Key('a$content'), key: Key('a$content'),
@ -68,12 +68,12 @@ class AccountProfileImage extends StatelessWidget {
if (content is String) { if (content is String) {
direct = content.startsWith('http'); direct = content.startsWith('http');
if (!isEmpty) isEmpty = content.isEmpty; if (!isEmpty) isEmpty = content.isEmpty;
if (!isEmpty) isEmpty = content.endsWith('/api/attachments/0'); if (!isEmpty) isEmpty = content.endsWith('/attachments/0');
} }
final url = direct final url = direct
? content ? content
: '${ServiceFinder.services['paperclip']}/api/attachments/$content'; : ServiceFinder.buildUrl('files', '/attachments/$content');
if (PlatformInfo.canCacheImage) { if (PlatformInfo.canCacheImage) {
return CachedNetworkImage( return CachedNetworkImage(

View File

@ -24,9 +24,9 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = GetConnect(); final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['passport']; client.httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
final resp = await client.get('/api/users/${widget.account.name}'); final resp = await client.get('/users/${widget.account.name}');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
_userinfo = Account.fromJson(resp.body); _userinfo = Account.fromJson(resp.body);
setState(() => _isBusy = false); setState(() => _isBusy = false);

View File

@ -123,7 +123,7 @@ class _ArticleDeletionDialogState extends State<ArticleDeletionDialog> {
final client = auth.configureClient('interactive'); final client = auth.configureClient('interactive');
setState(() => _isBusy = true); setState(() => _isBusy = true);
final resp = await client.delete('/api/articles/${widget.item.id}'); final resp = await client.delete('/articles/${widget.item.id}');
setState(() => _isBusy = false); setState(() => _isBusy = false);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {

View File

@ -192,8 +192,8 @@ class _ArticleItemState extends State<ArticleItem> {
item: widget.item, item: widget.item,
onReact: (symbol, changes) { onReact: (symbol, changes) {
setState(() { setState(() {
item.reactionList[symbol] = item.metric!.reactionList[symbol] =
(item.reactionList[symbol] ?? 0) + changes; (item.metric!.reactionList[symbol] ?? 0) + changes;
}); });
}, },
).paddingOnly( ).paddingOnly(

View File

@ -30,7 +30,7 @@ class _ArticleQuickActionState extends State<ArticleQuickAction> {
useRootNavigator: true, useRootNavigator: true,
context: context, context: context,
builder: (context) => PostReactionPopup( builder: (context) => PostReactionPopup(
reactionList: widget.item.reactionList, reactionList: widget.item.metric!.reactionList,
onReact: (key, value) { onReact: (key, value) {
doWidgetReact(key, value.attitude); doWidgetReact(key, value.attitude);
}, },
@ -50,7 +50,7 @@ class _ArticleQuickActionState extends State<ArticleQuickAction> {
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final resp = await client.post('/api/articles/${widget.item.alias}/react', { final resp = await client.post('/articles/${widget.item.alias}/react', {
'symbol': symbol, 'symbol': symbol,
'attitude': attitude, 'attitude': attitude,
}); });
@ -71,7 +71,7 @@ class _ArticleQuickActionState extends State<ArticleQuickAction> {
void initState() { void initState() {
super.initState(); super.initState();
if (!widget.isReactable && widget.item.reactionList.isEmpty) { if (!widget.isReactable && widget.item.metric!.reactionList.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onReact('thumb_up', 0); widget.onReact('thumb_up', 0);
}); });
@ -94,7 +94,7 @@ class _ArticleQuickActionState extends State<ArticleQuickAction> {
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: [ children: [
...widget.item.reactionList.entries.map((x) { ...widget.item.metric!.reactionList.entries.map((x) {
final info = reactions[x.key]; final info = reactions[x.key];
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),

View File

@ -82,7 +82,8 @@ class _AttachmentItemState extends State<AttachmentItem> {
), ),
onPressed: () { onPressed: () {
launchUrlString( launchUrlString(
'${ServiceFinder.services['paperclip']}/api/attachments/${widget.item.id}', ServiceFinder.buildUrl(
'files', '/attachments/${widget.item.id}'),
); );
}, },
), ),
@ -117,7 +118,7 @@ class _AttachmentItemImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Hero( return Hero(
tag: Key('a${item.uuid}p${parentId}'), tag: Key('a${item.uuid}p$parentId'),
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@ -125,7 +126,7 @@ class _AttachmentItemImage extends StatelessWidget {
CachedNetworkImage( CachedNetworkImage(
fit: fit, fit: fit,
imageUrl: imageUrl:
'${ServiceFinder.services['paperclip']}/api/attachments/${item.id}', ServiceFinder.buildUrl('files', '/attachments/${item.id}'),
progressIndicatorBuilder: (context, url, downloadProgress) => progressIndicatorBuilder: (context, url, downloadProgress) =>
Center( Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
@ -135,7 +136,7 @@ class _AttachmentItemImage extends StatelessWidget {
) )
else else
Image.network( Image.network(
'${ServiceFinder.services['paperclip']}/api/attachments/${item.id}', ServiceFinder.buildUrl('files', '/attachments/${item.id}'),
fit: fit, fit: fit,
loadingBuilder: (BuildContext context, Widget child, loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) { ImageChunkEvent? loadingProgress) {
@ -205,7 +206,7 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
super.initState(); super.initState();
_player.open( _player.open(
Media( Media(
'${ServiceFinder.services['paperclip']}/api/attachments/${widget.item.id}', ServiceFinder.buildUrl('files', '/attachments/${widget.item.id}'),
), ),
play: false, play: false,
); );

View File

@ -32,7 +32,7 @@ class _ChannelDeletionDialogState extends State<ChannelDeletionDialog> {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client final resp = await client
.delete('/api/channels/${widget.realm}/${widget.channel.id}'); .delete('/channels/${widget.realm}/${widget.channel.id}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
} else if (Navigator.canPop(context)) { } else if (Navigator.canPop(context)) {
@ -51,7 +51,7 @@ class _ChannelDeletionDialogState extends State<ChannelDeletionDialog> {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.delete( final resp = await client.delete(
'/api/channels/${widget.realm}/${widget.channel.alias}/members/me', '/channels/${widget.realm}/${widget.channel.alias}/members/me',
); );
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);

View File

@ -118,11 +118,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
); );
} else { } else {
return ListTile( return ListTile(
minTileHeight: item.realmId == null minTileHeight: widget.isDense ? 48 : null,
? 48
: widget.isDense
? 24
: null,
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: backgroundColor:
item.realmId == null ? Colors.indigo : Colors.transparent, item.realmId == null ? Colors.indigo : Colors.transparent,

View File

@ -43,7 +43,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
final client = ServiceFinder.configureClient('messaging'); final client = ServiceFinder.configureClient('messaging');
final resp = await client final resp = await client
.get('/api/channels/${widget.realm}/${widget.channel.alias}/members'); .get('/channels/${widget.realm}/${widget.channel.alias}/members');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
setState(() { setState(() {
_members = resp.body _members = resp.body
@ -79,7 +79,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.post( final resp = await client.post(
'/api/channels/${widget.realm}/${widget.channel.alias}/members', '/channels/${widget.realm}/${widget.channel.alias}/members',
{'target': username}, {'target': username},
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
@ -100,7 +100,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
final client = auth.configureClient('messaging'); final client = auth.configureClient('messaging');
final resp = await client.request( final resp = await client.request(
'/api/channels/${widget.realm}/${widget.channel.alias}/members', '/channels/${widget.realm}/${widget.channel.alias}/members',
'delete', 'delete',
body: {'target': item.account.name}, body: {'target': item.account.name},
); );

View File

@ -41,7 +41,7 @@ class _ChatCallButtonState extends State<ChatCallButton> {
? widget.realm?.alias ? widget.realm?.alias
: 'global'; : 'global';
final resp = await client.post( final resp = await client.post(
'/api/channels/$scope/${widget.channel.alias}/calls', '/channels/$scope/${widget.channel.alias}/calls',
{}, {},
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
@ -65,7 +65,7 @@ class _ChatCallButtonState extends State<ChatCallButton> {
? widget.realm?.alias ? widget.realm?.alias
: 'global'; : 'global';
final resp = await client final resp = await client
.delete('/api/channels/$scope/${widget.channel.alias}/calls/ongoing'); .delete('/channels/$scope/${widget.channel.alias}/calls/ongoing');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
if (widget.onEnded != null) widget.onEnded!(); if (widget.onEnded != null) widget.onEnded!();
} else { } else {

View File

@ -38,7 +38,7 @@ class _ChatEventDeletionDialogState extends State<ChatEventDeletionDialog> {
? widget.realm?.alias ? widget.realm?.alias
: 'global'; : 'global';
final resp = await client.delete( final resp = await client.delete(
'/api/channels/$scope/${widget.channel.alias}/messages/${widget.item.id}', '/channels/$scope/${widget.channel.alias}/messages/${widget.item.id}',
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
Navigator.pop(context, resp.body); Navigator.pop(context, resp.body);

View File

@ -114,12 +114,12 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
Response resp; Response resp;
if (_editTo != null) { if (_editTo != null) {
resp = await client.put( resp = await client.put(
'/api/channels/${widget.realm}/${widget.channel.alias}/messages/${_editTo!.id}', '/channels/${widget.realm}/${widget.channel.alias}/messages/${_editTo!.id}',
payload, payload,
); );
} else { } else {
resp = await client.post( resp = await client.post(
'/api/channels/${widget.realm}/${widget.channel.alias}/messages', '/channels/${widget.realm}/${widget.channel.alias}/messages',
payload, payload,
); );
} }

View File

@ -1,9 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/providers/account.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/chat.dart';
class BackgroundStateWidget extends StatelessWidget { class BackgroundStateWidget extends StatelessWidget {
const BackgroundStateWidget({super.key}); const BackgroundStateWidget({super.key});
@ -11,14 +10,11 @@ class BackgroundStateWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final AccountProvider account = Get.find(); final WebSocketProvider ws = Get.find();
final ChatProvider chat = Get.find();
return Obx(() { return Obx(() {
final disconnected = final disconnected = ws.isConnected.isFalse;
chat.isConnected.isFalse || account.isConnected.isFalse; final connecting = ws.isConnecting.isTrue;
final connecting =
chat.isConnecting.isTrue || account.isConnecting.isTrue;
return Row(children: [ return Row(children: [
if (disconnected && !connecting) if (disconnected && !connecting)
@ -30,10 +26,8 @@ class BackgroundStateWidget extends StatelessWidget {
} }
return IconButton( return IconButton(
tooltip: [ tooltip: [
if (account.isConnected.isFalse) if (ws.isConnected.isFalse)
'Lost Connection with Passport Server...', 'Lost Connection with Solar Network...',
if (chat.isConnected.isFalse)
'Lost Connection with Messaging Server...',
].join('\n'), ].join('\n'),
icon: const Icon(Icons.wifi_off) icon: const Icon(Icons.wifi_off)
.animate(onPlay: (c) => c.repeat()) .animate(onPlay: (c) => c.repeat())
@ -41,8 +35,7 @@ class BackgroundStateWidget extends StatelessWidget {
.then() .then()
.fadeOut(duration: 800.ms), .fadeOut(duration: 800.ms),
onPressed: () { onPressed: () {
if (account.isConnected.isFalse) account.connect(); if (ws.isConnected.isFalse) ws.connect();
if (chat.isConnected.isFalse) chat.connect();
}, },
); );
}, },
@ -56,10 +49,8 @@ class BackgroundStateWidget extends StatelessWidget {
} }
return IconButton( return IconButton(
tooltip: [ tooltip: [
if (account.isConnecting.isTrue) if (ws.isConnecting.isTrue)
'Waiting Passport Server Response...', 'Waiting Solar Network Connection...',
if (chat.isConnecting.isTrue)
'Waiting Messaging Server Response...',
].join('\n'), ].join('\n'),
icon: const Icon(Icons.sync) icon: const Icon(Icons.sync)
.animate(onPlay: (c) => c.repeat()) .animate(onPlay: (c) => c.repeat())

View File

@ -158,7 +158,7 @@ class _PostDeletionDialogState extends State<PostDeletionDialog> {
final client = auth.configureClient('interactive'); final client = auth.configureClient('interactive');
setState(() => _isBusy = true); setState(() => _isBusy = true);
final resp = await client.delete('/api/posts/${widget.item.id}'); final resp = await client.delete('/posts/${widget.item.id}');
setState(() => _isBusy = false); setState(() => _isBusy = false);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {

View File

@ -304,8 +304,8 @@ class _PostItemState extends State<PostItem> {
item: widget.item, item: widget.item,
onReact: (symbol, changes) { onReact: (symbol, changes) {
setState(() { setState(() {
item.reactionList[symbol] = item.metric!.reactionList[symbol] =
(item.reactionList[symbol] ?? 0) + changes; (item.metric!.reactionList[symbol] ?? 0) + changes;
}); });
}, },
).paddingOnly( ).paddingOnly(

View File

@ -33,7 +33,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
useRootNavigator: true, useRootNavigator: true,
context: context, context: context,
builder: (context) => PostReactionPopup( builder: (context) => PostReactionPopup(
reactionList: widget.item.reactionList, reactionList: widget.item.metric!.reactionList,
onReact: (key, value) { onReact: (key, value) {
doWidgetReact(key, value.attitude); doWidgetReact(key, value.attitude);
}, },
@ -53,7 +53,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final resp = await client.post('/api/posts/${widget.item.alias}/react', { final resp = await client.post('/posts/${widget.item.alias}/react', {
'symbol': symbol, 'symbol': symbol,
'attitude': attitude, 'attitude': attitude,
}); });
@ -74,7 +74,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
void initState() { void initState() {
super.initState(); super.initState();
if (!widget.isReactable && widget.item.reactionList.isEmpty) { if (!widget.isReactable && widget.item.metric!.reactionList.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onReact('thumb_up', 0); widget.onReact('thumb_up', 0);
}); });
@ -95,7 +95,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
if (widget.isReactable && widget.isShowReply) if (widget.isReactable && widget.isShowReply)
ActionChip( ActionChip(
avatar: const Icon(Icons.comment), avatar: const Icon(Icons.comment),
label: Text(widget.item.replyCount.toString()), label: Text(widget.item.metric!.replyCount.toString()),
visualDensity: density, visualDensity: density,
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
@ -119,7 +119,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: [ children: [
...widget.item.reactionList.entries.map((x) { ...widget.item.metric!.reactionList.entries.map((x) {
final info = reactions[x.key]; final info = reactions[x.key];
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),

View File

@ -27,9 +27,9 @@ class _RealmDeletionDialogState extends State<RealmDeletionDialog> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.delete('/api/realms/${widget.realm.id}'); final resp = await client.delete('/realms/${widget.realm.id}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
} else if (Navigator.canPop(context)) { } else if (Navigator.canPop(context)) {
@ -45,10 +45,10 @@ class _RealmDeletionDialogState extends State<RealmDeletionDialog> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = final resp =
await client.delete('/api/realms/${widget.realm.id}/members/me'); await client.delete('/realms/${widget.realm.id}/members/me');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
} else if (Navigator.canPop(context)) { } else if (Navigator.canPop(context)) {

View File

@ -38,9 +38,9 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
void getMembers() async { void getMembers() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = ServiceFinder.configureClient('passport'); final client = ServiceFinder.configureClient('auth');
final resp = await client.get('/api/realms/${widget.realm.alias}/members'); final resp = await client.get('/realms/${widget.realm.alias}/members');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
setState(() { setState(() {
_members = resp.body _members = resp.body
@ -73,10 +73,10 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.post( final resp = await client.post(
'/api/realms/${widget.realm.alias}/members', '/realms/${widget.realm.alias}/members',
{'target': username}, {'target': username},
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
@ -94,10 +94,10 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('passport'); final client = auth.configureClient('auth');
final resp = await client.request( final resp = await client.request(
'/api/realms/${widget.realm.alias}/members', '/realms/${widget.realm.alias}/members',
'delete', 'delete',
body: {'target': item.account.name}, body: {'target': item.account.name},
); );