mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 16:08:40 +00:00
follow request reject and block works
This commit is contained in:
parent
20c20eb1e1
commit
0da87f1712
18 changed files with 332 additions and 178 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/src/providers/api_provider.dart';
|
import 'package:twonly/src/providers/api_provider.dart';
|
||||||
import 'package:twonly/src/providers/db_provider.dart';
|
import 'package:twonly/src/providers/db_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:twonly/src/providers/notify_provider.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'src/app.dart';
|
import 'src/app.dart';
|
||||||
import 'src/settings/settings_controller.dart';
|
import 'src/settings/settings_controller.dart';
|
||||||
|
|
@ -49,5 +51,12 @@ void main() async {
|
||||||
// return true;
|
// return true;
|
||||||
// });
|
// });
|
||||||
|
|
||||||
runApp(MyApp(settingsController: settingsController));
|
runApp(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => NotifyProvider()),
|
||||||
|
],
|
||||||
|
child: MyApp(settingsController: settingsController),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/main.dart';
|
import 'package:twonly/main.dart';
|
||||||
|
import 'package:twonly/src/providers/notify_provider.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
import 'package:twonly/src/views/onboarding_view.dart';
|
import 'package:twonly/src/views/onboarding_view.dart';
|
||||||
import 'package:twonly/src/views/home_view.dart';
|
import 'package:twonly/src/views/home_view.dart';
|
||||||
|
|
@ -30,13 +32,18 @@ class _MyAppState extends State<MyApp> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Start the color animation
|
|
||||||
_startColorAnimation();
|
_startColorAnimation();
|
||||||
|
|
||||||
apiProvider.setConnectionStateCallback((isConnected) {
|
apiProvider.setConnectionStateCallback((isConnected) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isConnected = isConnected;
|
_isConnected = isConnected;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
apiProvider.setUpdatedContacts(() {
|
||||||
|
context.read<NotifyProvider>().update();
|
||||||
|
});
|
||||||
|
|
||||||
|
context.read<NotifyProvider>().update();
|
||||||
apiProvider.connect();
|
apiProvider.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
70
lib/src/components/message_send_state_icon.dart
Normal file
70
lib/src/components/message_send_state_icon.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
enum MessageSendState {
|
||||||
|
opened,
|
||||||
|
received,
|
||||||
|
send,
|
||||||
|
sending,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageSendStateIcon extends StatelessWidget {
|
||||||
|
final MessageSendState state;
|
||||||
|
|
||||||
|
const MessageSendStateIcon({super.key, required this.state});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget icon = Placeholder();
|
||||||
|
String text = "";
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case MessageSendState.opened:
|
||||||
|
icon = Icon(
|
||||||
|
Icons.crop_square,
|
||||||
|
size: 14,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
);
|
||||||
|
text = "Opened";
|
||||||
|
break;
|
||||||
|
case MessageSendState.received:
|
||||||
|
icon = Icon(
|
||||||
|
Icons.square_rounded,
|
||||||
|
size: 14,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
);
|
||||||
|
text = "Received";
|
||||||
|
break;
|
||||||
|
case MessageSendState.send:
|
||||||
|
icon = Icon(
|
||||||
|
Icons.send,
|
||||||
|
size: 14,
|
||||||
|
);
|
||||||
|
text = "Send";
|
||||||
|
break;
|
||||||
|
case MessageSendState.sending:
|
||||||
|
icon = Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
text = "Sending";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Text(text, style: TextStyle(fontSize: 12)),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/src/components/notification_badge.dart
Normal file
39
lib/src/components/notification_badge.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NotificationBadge extends StatelessWidget {
|
||||||
|
final int count;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const NotificationBadge(
|
||||||
|
{super.key, required this.count, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (count == 0) return child;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
Positioned(
|
||||||
|
right: 5,
|
||||||
|
top: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(5.0), // Add some padding
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red, // Background color
|
||||||
|
shape: BoxShape.circle, // Make it circular
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
count.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, // Text color
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
"searchUsernameInput": "Username",
|
"searchUsernameInput": "Username",
|
||||||
"searchUsernameTitle": "Search username",
|
"searchUsernameTitle": "Search username",
|
||||||
"searchUsernameNotFound": "Username not found",
|
"searchUsernameNotFound": "Username not found",
|
||||||
|
"searchUsernameNewFollowerTitle": "Follow requests",
|
||||||
|
"searchUsernameQrCodeBtn": "Scan QR code",
|
||||||
"searchUsernameNotFoundLong": "\"{username}\" is not a twonly user. Please check the username and try again.",
|
"searchUsernameNotFoundLong": "\"{username}\" is not a twonly user. Please check the username and try again.",
|
||||||
"errorUnknown": "An unexpected error has occurred. Please try again later.",
|
"errorUnknown": "An unexpected error has occurred. Please try again later.",
|
||||||
"errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.",
|
"errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ class DbContacts extends CvModelBase {
|
||||||
static const columnRequested = "requested";
|
static const columnRequested = "requested";
|
||||||
final requested = CvField<int>(columnRequested);
|
final requested = CvField<int>(columnRequested);
|
||||||
|
|
||||||
|
static const columnBlocked = "blocked";
|
||||||
|
final blocked = CvField<int>(columnBlocked);
|
||||||
|
|
||||||
static const columnCreatedAt = "created_at";
|
static const columnCreatedAt = "created_at";
|
||||||
final createdAt = CvField<DateTime>(columnCreatedAt);
|
final createdAt = CvField<DateTime>(columnCreatedAt);
|
||||||
|
|
||||||
|
|
@ -40,6 +43,7 @@ class DbContacts extends CvModelBase {
|
||||||
$columnDisplayName TEXT,
|
$columnDisplayName TEXT,
|
||||||
$columnAccepted INT NOT NULL DEFAULT 0,
|
$columnAccepted INT NOT NULL DEFAULT 0,
|
||||||
$columnRequested INT NOT NULL DEFAULT 0,
|
$columnRequested INT NOT NULL DEFAULT 0,
|
||||||
|
$columnBlocked INT NOT NULL DEFAULT 0,
|
||||||
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
""";
|
""";
|
||||||
|
|
@ -47,22 +51,23 @@ class DbContacts extends CvModelBase {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<CvField> get fields =>
|
List<CvField> get fields =>
|
||||||
[userId, displayName, accepted, requested, createdAt];
|
[userId, displayName, accepted, requested, blocked, createdAt];
|
||||||
|
|
||||||
static Future<List<Contact>> getUsers() async {
|
static Future<List<Contact>> getUsers() async {
|
||||||
try {
|
try {
|
||||||
var users = await dbProvider.db!.query(tableName, columns: [
|
var users = await dbProvider.db!.query(tableName,
|
||||||
columnUserId,
|
columns: [
|
||||||
columnDisplayName,
|
columnUserId,
|
||||||
columnAccepted,
|
columnDisplayName,
|
||||||
columnRequested,
|
columnAccepted,
|
||||||
columnCreatedAt
|
columnRequested,
|
||||||
]);
|
columnCreatedAt
|
||||||
|
],
|
||||||
|
where: "$columnBlocked = 0");
|
||||||
if (users.isEmpty) return [];
|
if (users.isEmpty) return [];
|
||||||
|
|
||||||
List<Contact> parsedUsers = [];
|
List<Contact> parsedUsers = [];
|
||||||
for (int i = 0; i < users.length; i++) {
|
for (int i = 0; i < users.length; i++) {
|
||||||
print(users[i]);
|
|
||||||
parsedUsers.add(
|
parsedUsers.add(
|
||||||
Contact(
|
Contact(
|
||||||
userId: Int64(users.cast()[i][columnUserId]),
|
userId: Int64(users.cast()[i][columnUserId]),
|
||||||
|
|
@ -79,6 +84,26 @@ class DbContacts extends CvModelBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future blockUser(int userId) async {
|
||||||
|
Map<String, dynamic> valuesToUpdate = {
|
||||||
|
columnBlocked: 1,
|
||||||
|
};
|
||||||
|
await dbProvider.db!.update(
|
||||||
|
tableName,
|
||||||
|
valuesToUpdate,
|
||||||
|
where: "$columnUserId = ?",
|
||||||
|
whereArgs: [userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future deleteUser(int userId) async {
|
||||||
|
await dbProvider.db!.delete(
|
||||||
|
tableName,
|
||||||
|
where: "$columnUserId = ?",
|
||||||
|
whereArgs: [userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<bool> insertNewContact(
|
static Future<bool> insertNewContact(
|
||||||
String username, int userId, bool requested) async {
|
String username, int userId, bool requested) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:twonly/src/utils/json.dart';
|
import 'package:twonly/src/utils/json.dart';
|
||||||
part 'message.g.dart';
|
part 'message.g.dart';
|
||||||
|
|
||||||
enum MessageKind { textMessage, image, video, contactRequest }
|
enum MessageKind { textMessage, image, video, contactRequest, rejectRequest }
|
||||||
|
|
||||||
// so _$MessageKindEnumMap gets generated
|
// so _$MessageKindEnumMap gets generated
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const _$MessageKindEnumMap = {
|
||||||
MessageKind.image: 'image',
|
MessageKind.image: 'image',
|
||||||
MessageKind.video: 'video',
|
MessageKind.video: 'video',
|
||||||
MessageKind.contactRequest: 'contactRequest',
|
MessageKind.contactRequest: 'contactRequest',
|
||||||
|
MessageKind.rejectRequest: 'rejectRequest',
|
||||||
};
|
};
|
||||||
|
|
||||||
Message _$MessageFromJson(Map<String, dynamic> json) => Message(
|
Message _$MessageFromJson(Map<String, dynamic> json) => Message(
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ class ApiProvider {
|
||||||
bool _tryingToConnect = false;
|
bool _tryingToConnect = false;
|
||||||
final log = Logger("api_provider");
|
final log = Logger("api_provider");
|
||||||
Function(bool)? _connectionStateCallback;
|
Function(bool)? _connectionStateCallback;
|
||||||
|
Function? _updatedContacts;
|
||||||
|
|
||||||
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
|
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
|
||||||
|
|
||||||
|
|
@ -109,6 +110,10 @@ class ApiProvider {
|
||||||
_connectionStateCallback = callBack;
|
_connectionStateCallback = callBack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setUpdatedContacts(Function callBack) {
|
||||||
|
_updatedContacts = callBack;
|
||||||
|
}
|
||||||
|
|
||||||
void tryToReconnect() {
|
void tryToReconnect() {
|
||||||
if (_tryingToConnect) return;
|
if (_tryingToConnect) return;
|
||||||
_tryingToConnect = true;
|
_tryingToConnect = true;
|
||||||
|
|
@ -157,13 +162,22 @@ class ApiProvider {
|
||||||
Int64 fromUserId = msg.v0.newMessage.fromUserId;
|
Int64 fromUserId = msg.v0.newMessage.fromUserId;
|
||||||
Message? message = await SignalHelper.getDecryptedText(fromUserId, body);
|
Message? message = await SignalHelper.getDecryptedText(fromUserId, body);
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
Result username = await getUsername(fromUserId);
|
switch (message.kind) {
|
||||||
if (username.isSuccess) {
|
case MessageKind.contactRequest:
|
||||||
print(username.value);
|
Result username = await getUsername(fromUserId);
|
||||||
Uint8List name = username.value.userdata.username;
|
if (username.isSuccess) {
|
||||||
DbContacts.insertNewContact(
|
Uint8List name = username.value.userdata.username;
|
||||||
utf8.decode(name), fromUserId.toInt(), true);
|
DbContacts.insertNewContact(
|
||||||
print(message);
|
utf8.decode(name), fromUserId.toInt(), true);
|
||||||
|
updateNotifier();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MessageKind.rejectRequest:
|
||||||
|
DbContacts.deleteUser(fromUserId.toInt());
|
||||||
|
updateNotifier();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.shout("Got unknown MessageKind $message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ok = client.Response_Ok()..none = true;
|
var ok = client.Response_Ok()..none = true;
|
||||||
|
|
@ -182,6 +196,12 @@ class ApiProvider {
|
||||||
_channel!.sink.add(resBytes);
|
_channel!.sink.add(resBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future updateNotifier() async {
|
||||||
|
if (_updatedContacts != null) {
|
||||||
|
_updatedContacts!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
|
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
|
||||||
final startTime = DateTime.now();
|
final startTime = DateTime.now();
|
||||||
|
|
||||||
|
|
|
||||||
29
lib/src/providers/notify_provider.dart
Normal file
29
lib/src/providers/notify_provider.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
|
|
||||||
|
/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
|
||||||
|
// ignore: prefer_mixin
|
||||||
|
class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
|
int _newContactRequests = 0;
|
||||||
|
List<Contact> _allContacts = [];
|
||||||
|
|
||||||
|
int get newContactRequests => _newContactRequests;
|
||||||
|
List<Contact> get allContacts => _allContacts;
|
||||||
|
|
||||||
|
void update() async {
|
||||||
|
_allContacts = await DbContacts.getUsers();
|
||||||
|
|
||||||
|
_newContactRequests = _allContacts
|
||||||
|
.where((contact) => !contact.accepted && contact.requested)
|
||||||
|
.length;
|
||||||
|
print(_newContactRequests);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes `Counter` readable inside the devtools by listing all of its properties
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(IntProperty('count', newContactRequests));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,7 +38,6 @@ class ConnectPreKeyStore extends PreKeyStore {
|
||||||
@override
|
@override
|
||||||
Future<void> storePreKey(int preKeyId, PreKeyRecord record) async {
|
Future<void> storePreKey(int preKeyId, PreKeyRecord record) async {
|
||||||
if (!await containsPreKey(preKeyId)) {
|
if (!await containsPreKey(preKeyId)) {
|
||||||
print(preKeyId);
|
|
||||||
await dbProvider.db!.insert(DB.tableName,
|
await dbProvider.db!.insert(DB.tableName,
|
||||||
{DB.columnPreKeyId: preKeyId, DB.columnPreKey: record.serialize()});
|
{DB.columnPreKeyId: preKeyId, DB.columnPreKey: record.serialize()});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:twonly/main.dart';
|
import 'package:twonly/main.dart';
|
||||||
|
|
@ -44,6 +45,25 @@ Future<bool> addNewContact(String username) async {
|
||||||
return res.isSuccess;
|
return res.isSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
|
||||||
|
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
|
||||||
|
|
||||||
|
if (bytes == null) {
|
||||||
|
Logger("utils/api").shout("Error encryption message!");
|
||||||
|
return Result.error(ErrorCode.InternalError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result resp = await apiProvider.sendTextMessage(userId, bytes);
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result> rejectUserRequest(Int64 userId) async {
|
||||||
|
Message msg =
|
||||||
|
Message(kind: MessageKind.rejectRequest, timestamp: DateTime.now());
|
||||||
|
return encryptAndSendMessage(userId, msg);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result> createNewUser(String username, String inviteCode) async {
|
Future<Result> createNewUser(String username, String inviteCode) async {
|
||||||
final storage = getSecureStorage();
|
final storage = getSecureStorage();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import 'package:twonly/src/components/initialsavatar_component.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/components/initialsavatar.dart';
|
||||||
|
import 'package:twonly/src/components/message_send_state_icon.dart';
|
||||||
|
import 'package:twonly/src/components/notification_badge.dart';
|
||||||
|
import 'package:twonly/src/providers/notify_provider.dart';
|
||||||
import 'package:twonly/src/views/search_username_view.dart';
|
import 'package:twonly/src/views/search_username_view.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'new_message_view.dart';
|
import 'new_message_view.dart';
|
||||||
|
|
@ -7,8 +10,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'chat_item_details_view.dart';
|
import 'chat_item_details_view.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
enum MessageSendState { sending, send, opened, received }
|
|
||||||
|
|
||||||
class ChatItem {
|
class ChatItem {
|
||||||
const ChatItem(
|
const ChatItem(
|
||||||
{required this.username,
|
{required this.username,
|
||||||
|
|
@ -57,19 +58,12 @@ class ChatListView extends StatefulWidget {
|
||||||
|
|
||||||
class _ChatListViewState extends State<ChatListView> {
|
class _ChatListViewState extends State<ChatListView> {
|
||||||
int _secondsSinceOpen = 0;
|
int _secondsSinceOpen = 0;
|
||||||
int _newContactRequests = 0;
|
|
||||||
late Timer _timer;
|
late Timer _timer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_startTimer();
|
_startTimer();
|
||||||
_checkNewContactRequests();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _checkNewContactRequests() async {
|
|
||||||
_newContactRequests = (await DbContacts.getUsers()).length;
|
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startTimer() {
|
void _startTimer() {
|
||||||
|
|
@ -101,57 +95,12 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getMessageSateIcon(MessageSendState state) {
|
|
||||||
List<Widget> children = [];
|
|
||||||
Widget icon = Placeholder();
|
|
||||||
String text = "";
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case MessageSendState.opened:
|
|
||||||
icon = Icon(
|
|
||||||
Icons.crop_square,
|
|
||||||
size: 14,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
);
|
|
||||||
text = "Opened";
|
|
||||||
break;
|
|
||||||
case MessageSendState.received:
|
|
||||||
icon = Icon(Icons.square_rounded,
|
|
||||||
size: 14, color: Theme.of(context).colorScheme.primary);
|
|
||||||
text = "Received";
|
|
||||||
break;
|
|
||||||
case MessageSendState.send:
|
|
||||||
icon = Icon(Icons.send, size: 14);
|
|
||||||
text = "Send";
|
|
||||||
break;
|
|
||||||
case MessageSendState.sending:
|
|
||||||
icon = Row(children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 1,
|
|
||||||
)),
|
|
||||||
SizedBox(width: 2)
|
|
||||||
]);
|
|
||||||
text = "Sending";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
children.add(const SizedBox(width: 5));
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
icon,
|
|
||||||
const SizedBox(width: 3),
|
|
||||||
Text(text, style: TextStyle(fontSize: 12)),
|
|
||||||
const SizedBox(width: 5)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget getSubtitle(ChatItem item) {
|
Widget getSubtitle(ChatItem item) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
getMessageSateIcon(item.state),
|
MessageSendStateIcon(
|
||||||
|
state: item.state,
|
||||||
|
),
|
||||||
Text("•"),
|
Text("•"),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Text(formatDuration(item.lastMessageInSeconds + _secondsSinceOpen),
|
Text(formatDuration(item.lastMessageInSeconds + _secondsSinceOpen),
|
||||||
|
|
@ -180,62 +129,42 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context)!.chatsTitle),
|
title: Text(AppLocalizations.of(context)!.chatsTitle),
|
||||||
actions: [
|
actions: [
|
||||||
Stack(
|
NotificationBadge(
|
||||||
children: [
|
count: context.watch<NotifyProvider>().newContactRequests,
|
||||||
IconButton(
|
child: IconButton(
|
||||||
icon: Icon(Icons.person_add), // User with add icon
|
icon: Icon(Icons.person_add),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => SearchUsernameView(),
|
builder: (context) => SearchUsernameView(),
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_newContactRequests > 0)
|
|
||||||
Positioned(
|
|
||||||
right: 5,
|
|
||||||
top: 0,
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(5.0), // Add some padding
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red, // Background color
|
|
||||||
shape: BoxShape.circle, // Make it circular
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
_newContactRequests.toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white, // Text color
|
|
||||||
fontSize: 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
restorationId: 'sampleItemListView',
|
restorationId: 'chat_list_view',
|
||||||
itemCount: widget.items.length,
|
itemCount: widget.items.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final item = widget.items[index];
|
final item = widget.items[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(item.username),
|
title: Text(item.username),
|
||||||
subtitle: getSubtitle(item),
|
subtitle: getSubtitle(item),
|
||||||
leading: InitialsAvatar(displayName: item.username),
|
leading: InitialsAvatar(displayName: item.username),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => SampleItemDetailsView(
|
builder: (context) => SampleItemDetailsView(
|
||||||
userId: item.userId,
|
userId: item.userId,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:twonly/src/components/initialsavatar_component.dart';
|
import 'package:twonly/src/components/initialsavatar.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
import 'package:twonly/src/views/search_username_view.dart';
|
import 'package:twonly/src/views/search_username_view.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:twonly/src/components/initialsavatar.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
|
import 'package:twonly/src/providers/notify_provider.dart';
|
||||||
import 'package:twonly/src/utils/api.dart';
|
import 'package:twonly/src/utils/api.dart';
|
||||||
import 'package:twonly/src/views/register_view.dart';
|
import 'package:twonly/src/views/register_view.dart';
|
||||||
|
|
||||||
|
|
@ -28,11 +31,9 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
Logger("search_user_name").warning("Replace instead of pop");
|
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
if (status) {
|
if (status) {
|
||||||
// Navigator.pop(context);
|
context.read<NotifyProvider>().update();
|
||||||
} else if (context.mounted) {
|
} else if (context.mounted) {
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
context,
|
context,
|
||||||
|
|
@ -88,17 +89,19 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
||||||
showAlertDialog(context, "Coming soon",
|
showAlertDialog(context, "Coming soon",
|
||||||
"This feature is not yet implemented!");
|
"This feature is not yet implemented!");
|
||||||
},
|
},
|
||||||
label: Text("QR-Code scannen"),
|
label:
|
||||||
|
Text(AppLocalizations.of(context)!.searchUsernameQrCodeBtn),
|
||||||
),
|
),
|
||||||
SizedBox(height: 30),
|
SizedBox(height: 30),
|
||||||
Container(
|
if (context.read<NotifyProvider>().allContacts.isNotEmpty)
|
||||||
alignment: Alignment.centerLeft,
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 10),
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 10),
|
||||||
"Neue Followanfragen",
|
child: Text(
|
||||||
style: TextStyle(fontSize: 20),
|
AppLocalizations.of(context)!.searchUsernameNewFollowerTitle,
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ContactsListView(),
|
child: ContactsListView(),
|
||||||
)
|
)
|
||||||
|
|
@ -126,53 +129,55 @@ class ContactsListView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContactsListViewState extends State<ContactsListView> {
|
class _ContactsListViewState extends State<ContactsListView> {
|
||||||
List<Contact> _allContacts = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadContacts();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _loadContacts() async {
|
|
||||||
List<Contact> allContacts = await DbContacts.getUsers();
|
|
||||||
_allContacts = allContacts.where((contact) => !contact.accepted).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
List<Contact> contacts = context.read<NotifyProvider>().allContacts;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: _allContacts.length,
|
itemCount: contacts.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final contact = _allContacts[index];
|
final contact = contacts[index];
|
||||||
|
|
||||||
if (!contact.requested) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(contact.displayName),
|
|
||||||
subtitle: Text('Pending'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(contact.displayName),
|
title: Text(contact.displayName),
|
||||||
|
leading: InitialsAvatar(displayName: contact.displayName),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: (!contact.requested)
|
||||||
IconButton(
|
? [Text('Pending')]
|
||||||
icon: Icon(Icons.close, color: Colors.red),
|
: [
|
||||||
onPressed: () {
|
Tooltip(
|
||||||
// Handle reject action
|
message: "Block the user without informing.",
|
||||||
print('Rejected ${contact.displayName}');
|
child: IconButton(
|
||||||
},
|
icon: Icon(Icons.person_off_rounded,
|
||||||
),
|
color: const Color.fromARGB(164, 244, 67, 54)),
|
||||||
IconButton(
|
onPressed: () async {
|
||||||
icon: Icon(Icons.check, color: Colors.green),
|
await DbContacts.blockUser(contact.userId.toInt());
|
||||||
onPressed: () {
|
if (context.mounted) {
|
||||||
// Handle accept action
|
context.read<NotifyProvider>().update();
|
||||||
print('Accepted ${contact.displayName}');
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Reject the request and let the requester know.",
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.close, color: Colors.red),
|
||||||
|
onPressed: () async {
|
||||||
|
await DbContacts.deleteUser(contact.userId.toInt());
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<NotifyProvider>().update();
|
||||||
|
}
|
||||||
|
rejectUserRequest(contact.userId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.check, color: Colors.green),
|
||||||
|
onPressed: () {
|
||||||
|
// Handle accept action
|
||||||
|
print('Accepted ${contact.displayName}');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'dart:collection';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:twonly/src/components/initialsavatar_component.dart';
|
import 'package:twonly/src/components/initialsavatar.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
|
|
||||||
class ShareImageView extends StatefulWidget {
|
class ShareImageView extends StatefulWidget {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue