diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84fc1f6..d4f9445 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,24 @@
+## [1.3.0-beta](https://github.com/DrSolidDevil/Vidar/compare/v1.2.2-beta...v1.3.0-beta) (2025-08-17)
+
+> This version of Vidar brings you quality of life and performance improvements.
+
+### New Features
+* Added option to display time using the 12-hour clock (a.m./p.m.). [`3ed6a35`](https://github.com/DrSolidDevil/Vidar/commit/3ed6a35010102d115f21d0e8af6b498ecff332b9)
+* You will now be prompted sometimes to give feedback. This feedback is sent via mail. This request for feedback can be disabled in settings. [`95d8987`](https://github.com/DrSolidDevil/Vidar/commit/95d89875bc9ae195b267ad7b309beb7a1e8ec5d8) [`1c505ae`](https://github.com/DrSolidDevil/Vidar/commit/1c505ae314404842ecf8794e413ac9b7b788578a)
+
+### Performance Improvements
+* Rewrote message querying and rendering to be able to handle long conversations much better. [`22e1c12`](https://github.com/DrSolidDevil/Vidar/commit/22e1c126905036c7dbd16ec0d2fd16f25d662a83)
+* Rewrote the SMS permission request to utilize `FutureBuilder`. [`22e1c12`](https://github.com/DrSolidDevil/Vidar/commit/22e1c126905036c7dbd16ec0d2fd16f25d662a83) [`43a087b`](https://github.com/DrSolidDevil/Vidar/commit/43a087b6cf696e5c747d59a24f88fb342bdeaac6)
+
+### Other Changes
+* Message bar now expands when writing a long/multiline message. When the message is to long for the additional lines visible on screen then it starts to scroll. This feature makes you able to write long messages without headache. [`2a7a750`](https://github.com/DrSolidDevil/Vidar/commit/2a7a750df0a29ddc121ac5b15a1ab34ae5a7c086)
+* Dialogs/popups now scroll when its text is very long. [`2a7a750`](https://github.com/DrSolidDevil/Vidar/commit/2a7a750df0a29ddc121ac5b15a1ab34ae5a7c086)
+* Added scrollbar to settings page. [`2d6e222`](https://github.com/DrSolidDevil/Vidar/commit/2d6e22202cb1dc56613713e17a61844a006fc38f)
+* Visual improvements. [`bab8afd`](https://github.com/DrSolidDevil/Vidar/commit/bab8afdc284a7cf5e0f259a7d6b5bf392fd4212c) [`f54f40b`](https://github.com/DrSolidDevil/Vidar/commit/f54f40b2cc98ca2def8d031c707ca42749e12011)
+* Updated screenshots [`7620b55`](https://github.com/DrSolidDevil/Vidar/commit/7620b55f699dfa3288634938542fe71682467f3d)
+
+
+
## [1.2.2-beta](https://github.com/DrSolidDevil/Vidar/compare/v1.2.1-beta...v1.2.2-beta) (2025-08-02)
> Bug fix on the edit contact page.
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 917b133..33ec726 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -11,6 +11,14 @@
+
+
+
+
+
+
+
+
when (call.method) {
"querySms" -> {
- result.success(querySms(context, call.argument("phoneNumber")))
+ result.success(querySms(context, call.argument("phoneNumber"), call.argument("latestN")))
}
"sendSms" -> {
diff --git a/android/app/src/main/kotlin/com/drsoliddevil/vidar/SmsQuery.kt b/android/app/src/main/kotlin/com/drsoliddevil/vidar/SmsQuery.kt
index 50a4e7b..5940702 100644
--- a/android/app/src/main/kotlin/com/drsoliddevil/vidar/SmsQuery.kt
+++ b/android/app/src/main/kotlin/com/drsoliddevil/vidar/SmsQuery.kt
@@ -6,7 +6,7 @@ import androidx.core.net.toUri
import android.provider.Telephony.TextBasedSmsColumns
-fun querySms(context: Context, phoneNumber: String?): ArrayList>? {
+fun querySms(context: Context, phoneNumber: String?, latestN: Int?): ArrayList>? {
val sms: Cursor?
if (phoneNumber != null) {
sms = context.contentResolver.query(
@@ -14,7 +14,7 @@ fun querySms(context: Context, phoneNumber: String?): ArrayList = arrayOf(
@@ -49,7 +49,7 @@ private val includedQueryData: Array = arrayOf(
// Closes the cursor when it's done
// A list of hashmaps in kotlin is equivalent to a list of maps in dart
-private fun cursorToListOfHashMap(cursor: Cursor): ArrayList> {
+private fun cursorToListOfHashMap(cursor: Cursor, latestN: Int?): ArrayList> {
val threadIdColumnIndex: Int = cursor.getColumnIndexOrThrow(TextBasedSmsColumns.THREAD_ID)
val typeColumnIndex: Int = cursor.getColumnIndexOrThrow(TextBasedSmsColumns.TYPE)
val addressColumnIndex: Int = cursor.getColumnIndexOrThrow(TextBasedSmsColumns.ADDRESS)
@@ -64,9 +64,20 @@ private fun cursorToListOfHashMap(cursor: Cursor): ArrayList> = ArrayList>()
+ var i: Int = 0
+ println("latestN = $latestN")
@Suppress("ConvertTryFinallyToUseCall")
try {
do {
+ println(i)
+ if (latestN != null) {
+ if (i >= latestN) {
+ break
+ } else {
+ ++i
+ }
+ }
+
val entry: MutableMap = mutableMapOf()
entry[TextBasedSmsColumns.THREAD_ID] = cursor.getString(threadIdColumnIndex)
entry[TextBasedSmsColumns.TYPE] = cursor.getString(typeColumnIndex)
diff --git a/devtools_options.yaml b/devtools_options.yaml
index fa0b357..4dcfde9 100644
--- a/devtools_options.yaml
+++ b/devtools_options.yaml
@@ -1,3 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+ - shared_preferences: true
\ No newline at end of file
diff --git a/lib/configuration.dart b/lib/configuration.dart
index 2de75ba..64b8f7b 100644
--- a/lib/configuration.dart
+++ b/lib/configuration.dart
@@ -23,6 +23,7 @@ class SizeConfiguration {
static const double messageVerticalSeperation = 5;
static const double settingInfoText = 12;
static const double loadingFontSize = 32;
+ static const double feedbackFormFontSize = 12;
}
class TimeConfiguration {
@@ -36,6 +37,17 @@ class TimeConfiguration {
}
class MiscellaneousConfiguration {
+ // Probability of the user seeing a dialog prompting them to give feedback 0-1,
+ // where 1 means showing it always.
+ static const double userFeedbackDialogProbability = 0.0065; // 0.65% chance
+ static const Duration userFeedbackDialogPopupWait = const Duration(
+ seconds: 1,
+ );
+ static const String userFeedbackEmailAddress =
+ "drsoliddevil+vidarfeedback@gmail.com";
+}
+
+class ChatConfiguration {
static const String errorPrefix = "⚠";
static const List messageHints = [
"Write them a message!",
@@ -44,6 +56,8 @@ class MiscellaneousConfiguration {
"Start gossiping...",
"Talk to them, they miss you.",
];
+ // Number of messages to check during chat update
+ static const int numCheckDuringUpdate = 5;
}
class LoggingConfiguration {
diff --git a/lib/main.dart b/lib/main.dart
index 15e70c0..d66b2e7 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -3,13 +3,11 @@ import "package:permission_handler/permission_handler.dart";
import "package:shared_preferences/shared_preferences.dart";
import "package:vidar/configuration.dart";
import "package:vidar/pages/contact_list.dart";
-import "package:vidar/pages/no_sms_permission.dart";
import "package:vidar/utils/common_object.dart";
import "package:vidar/utils/settings.dart";
import "package:vidar/utils/sms.dart";
import "package:vidar/utils/storage.dart";
-
-late PermissionStatus smsStatus;
+import "package:vidar/widgets/info_text_widget.dart";
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -31,7 +29,6 @@ void main() async {
wipeSecureStorage();
}
SmsConstants(await retrieveSmsConstantsMap());
- smsStatus = await Permission.sms.request();
CommonObject.lastLogon = DateTime.now();
await (await SharedPreferences.getInstance()).setString(
"lastlogon",
@@ -45,11 +42,33 @@ class VidarApp extends StatelessWidget {
const VidarApp({super.key});
@override
- Widget build(final BuildContext context) {
- if (smsStatus.isGranted) {
- return const MaterialApp(title: "Vidar", home: ContactListPage());
- } else {
- return const MaterialApp(title: "Vidar", home: NoSmsPermissionPage());
- }
- }
+ Widget build(final BuildContext context) => MaterialApp(
+ title: "Vidar",
+ home: FutureBuilder(
+ future: Permission.sms.request(),
+ builder:
+ (
+ final BuildContext context,
+ final AsyncSnapshot snapshot,
+ ) {
+ if (snapshot.hasData) {
+ if (snapshot.data!.isGranted) {
+ return const ContactListPage();
+ } else {
+ return const InfoText(
+ text:
+ "To use Vidar you need to enable SMS permissions. Enable SMS permissions in the app settings then restart the app.",
+ textWidthFactor: 0.8,
+ fontSize: 20,
+ );
+ }
+ } else {
+ return const InfoText(
+ text: "Requesting SMS permission",
+ textWidthFactor: 0.8,
+ );
+ }
+ },
+ ),
+ );
}
diff --git a/lib/pages/contact_list.dart b/lib/pages/contact_list.dart
index db2802e..df03960 100644
--- a/lib/pages/contact_list.dart
+++ b/lib/pages/contact_list.dart
@@ -1,8 +1,13 @@
+import "dart:math";
+
import "package:flutter/material.dart";
+import "package:vidar/configuration.dart";
import "package:vidar/pages/edit_contact.dart";
+import "package:vidar/pages/feedback_page.dart";
import "package:vidar/pages/settings_page.dart";
import "package:vidar/utils/common_object.dart";
import "package:vidar/utils/contact.dart";
+import "package:vidar/utils/extended_change_notifier.dart";
import "package:vidar/utils/navigation.dart";
import "package:vidar/utils/settings.dart";
@@ -14,75 +19,149 @@ class ContactListPage extends StatefulWidget {
}
class _ContactListPageState extends State {
- _ContactListPageState();
-
- @override
- void initState() {
- super.initState();
+ _ContactListPageState() {
+ feedbackDialog = Stack(
+ children: [
+ AlertDialog(
+ title: Text(
+ "Feedback",
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
+ content: RichText(
+ text: TextSpan(
+ children: const [
+ TextSpan(
+ text:
+ "Vidar is still in development. We would appreciate your help in making Vidar better. Please tell us what you think!",
+ ),
+ TextSpan(
+ text: "\n\nTo disable these popups, go to settings.",
+ style: TextStyle(
+ fontStyle: FontStyle.italic,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ feedbackDialogVisible = false;
+ changeNotifier.notifyListeners();
+ },
+ child: Text(
+ "Dismiss",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ TextButton(
+ onPressed: () =>
+ clearNavigatorAndPush(context, const FeedbackPage()),
+ child: Text(
+ "Give Feedback",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ ],
+ backgroundColor: Settings.colorSet.dialogBackground,
+ ),
+ ],
+ );
}
+ late final Widget feedbackDialog;
+ final ExtendedChangeNotifier changeNotifier = ExtendedChangeNotifier();
+ bool feedbackDialogVisible = true;
+
@override
Widget build(final BuildContext context) {
- return Scaffold(
- backgroundColor: Settings.colorSet.secondary,
- floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
- floatingActionButton: DecoratedBox(
- decoration: BoxDecoration(
- color: Settings.colorSet.floatingActionButton,
- border: Border.all(color: Settings.colorSet.text, width: 2),
- borderRadius: BorderRadius.circular(10),
- ),
- child: FloatingActionButton(
- elevation: 0,
- highlightElevation: 0,
- onPressed: () {
- final Contact newContact = Contact("", "", "");
- clearNavigatorAndPush(
- context,
- EditContactPage(newContact, ContactPageCaller.newContact),
- );
- },
- backgroundColor: Colors.transparent,
- foregroundColor: Settings.colorSet.text,
- child: const Icon(Icons.add_comment),
- ),
- ),
+ return ListenableBuilder(
+ listenable: changeNotifier,
+ builder: (final BuildContext context, final Widget? value) {
+ final bool displayFeedbackDialog =
+ Settings.allowUserFeedbackDialog &&
+ feedbackDialogVisible &&
+ MiscellaneousConfiguration.userFeedbackDialogProbability >
+ Random().nextDouble();
- body: ListenableBuilder(
- listenable: CommonObject.contactList,
- builder: (final BuildContext context, final Widget? child) {
- return Material(
- color: Colors.transparent,
- child: ListView(
- children: CommonObject.contactList.getContactBadges(),
- ),
- );
- },
- ),
+ return Stack(
+ children: [
+ Scaffold(
+ backgroundColor: Settings.colorSet.secondary,
+ floatingActionButtonLocation:
+ FloatingActionButtonLocation.endFloat,
+ floatingActionButton: DecoratedBox(
+ decoration: BoxDecoration(
+ color: Settings.colorSet.floatingActionButton,
+ border: Border.all(color: Settings.colorSet.text, width: 2),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: FloatingActionButton(
+ elevation: 0,
+ highlightElevation: 0,
+ onPressed: () {
+ final Contact newContact = Contact("", "", "");
+ clearNavigatorAndPush(
+ context,
+ EditContactPage(newContact, ContactPageCaller.newContact),
+ );
+ },
+ backgroundColor: Colors.transparent,
+ foregroundColor: Settings.colorSet.text,
+ child: const Icon(Icons.add_comment),
+ ),
+ ),
+ body: ListenableBuilder(
+ listenable: CommonObject.contactList,
+ builder: (final BuildContext context, final Widget? value) {
+ return Material(
+ color: Colors.transparent,
+ child: ListView(
+ children: CommonObject.contactList.getContactBadges(),
+ ),
+ );
+ },
+ ),
- appBar: AppBar(
- backgroundColor: Settings.colorSet.primary,
- title: Text(
- "Vidar - For Privacy's Sake",
- style: TextStyle(
- fontSize: 18,
- color: Settings.colorSet.text,
- decoration: TextDecoration.none,
- ),
- ),
- actions: [
- Container(
- margin: const EdgeInsets.only(right: 10),
- child: IconButton(
- onPressed: () {
- clearNavigatorAndPush(context, const SettingsPage());
- },
- icon: Icon(Icons.settings, color: Settings.colorSet.text),
- tooltip: "Settings",
+ appBar: AppBar(
+ backgroundColor: Settings.colorSet.primary,
+ title: Text(
+ "Vidar – For Privacy's Sake",
+ style: TextStyle(
+ fontSize: 18,
+ color: Settings.colorSet.text,
+ decoration: TextDecoration.none,
+ ),
+ ),
+ actions: [
+ Container(
+ margin: const EdgeInsets.only(right: 10),
+ child: IconButton(
+ onPressed: () {
+ clearNavigatorAndPush(context, const SettingsPage());
+ },
+ icon: Icon(Icons.settings, color: Settings.colorSet.text),
+ tooltip: "Settings",
+ ),
+ ),
+ ],
+ ),
),
- ),
- ],
- ),
+ if (displayFeedbackDialog)
+ const ColoredBox(color: Colors.black54, child: SizedBox.expand()),
+ if (displayFeedbackDialog) feedbackDialog,
+ ],
+ );
+ },
);
}
}
diff --git a/lib/pages/edit_contact.dart b/lib/pages/edit_contact.dart
index 7b2c944..8077ad0 100644
--- a/lib/pages/edit_contact.dart
+++ b/lib/pages/edit_contact.dart
@@ -108,16 +108,29 @@ class _EditContactPageState extends State {
context: context,
builder: (final BuildContext context) {
return AlertDialog(
- title: const Text("Invalid details"),
- content: const Text("Please ensure edited details are correct"),
+ title: Text(
+ "Invalid details",
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
+ content: Text(
+ "Please ensure edited details are correct",
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
actions: [
TextButton(
onPressed: () {
clearNavigatorAndPush(context, const ContactListPage());
},
- child: const Text("OK"),
+ child: Text(
+ "OK",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
),
],
+ backgroundColor: Settings.colorSet.dialogBackground,
);
},
);
@@ -140,18 +153,29 @@ class _EditContactPageState extends State {
context: context,
builder: (final BuildContext context) {
return AlertDialog(
- title: const Text("Invalid details"),
- content: const Text(
+ title: Text(
+ "Invalid details",
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
+ content: Text(
"Please enter all details correctly to create a new contact",
+ style: TextStyle(color: Settings.colorSet.dialogText),
),
actions: [
TextButton(
onPressed: () {
clearNavigatorAndPush(context, const ContactListPage());
},
- child: const Text("OK"),
+ child: Text(
+ "OK",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
),
],
+ backgroundColor: Settings.colorSet.dialogBackground,
);
},
);
diff --git a/lib/pages/feedback_page.dart b/lib/pages/feedback_page.dart
new file mode 100644
index 0000000..08bcd34
--- /dev/null
+++ b/lib/pages/feedback_page.dart
@@ -0,0 +1,174 @@
+import "package:flutter/material.dart";
+import "package:url_launcher/url_launcher.dart";
+import "package:vidar/configuration.dart";
+import "package:vidar/pages/contact_list.dart";
+import "package:vidar/utils/common_object.dart";
+import "package:vidar/utils/navigation.dart";
+import "package:vidar/utils/settings.dart";
+import "package:vidar/utils/url.dart";
+import "package:vidar/widgets/buttons.dart";
+import "package:vidar/widgets/error_popup.dart";
+
+class FeedbackPage extends StatefulWidget {
+ const FeedbackPage({super.key});
+
+ @override
+ _FeedbackPageState createState() => _FeedbackPageState();
+}
+
+class _FeedbackPageState extends State {
+ String body = "";
+ final ScrollController _scrollController = ScrollController();
+
+ @override
+ Widget build(final BuildContext context) {
+ final double formWidth = MediaQuery.of(context).size.width;
+ final double formHeight = MediaQuery.of(context).size.height / 3.2;
+
+ return Scaffold(
+ backgroundColor: Settings.colorSet.primary,
+ appBar: AppBar(
+ backgroundColor: Settings.colorSet.secondary,
+ title: Text(
+ "Vidar – For Privacy's Sake",
+ style: TextStyle(
+ fontSize: 18,
+ color: Settings.colorSet.text,
+ decoration: TextDecoration.none,
+ ),
+ ),
+
+ leading: Container(
+ margin: const EdgeInsets.only(left: 10),
+ child: IconButton(
+ onPressed: () {
+ clearNavigatorAndPush(context, const ContactListPage());
+ },
+ icon: Icon(Icons.arrow_back, color: Settings.colorSet.text),
+ tooltip: "Go back",
+ ),
+ ),
+ ),
+ body: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 20),
+ child: Text(
+ "Feedback",
+ style: TextStyle(color: Settings.colorSet.text, fontSize: 24),
+ ),
+ ),
+ Text(
+ "Sent via mail.",
+ style: TextStyle(
+ fontStyle: FontStyle.italic,
+ color: Settings.colorSet.text,
+ ),
+ ),
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 30,
+ right: 30,
+ top: 30,
+ bottom: 20,
+ ),
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: Settings.colorSet.secondary,
+ width: 3,
+ ),
+ borderRadius: BorderRadius.circular(10),
+ color: Settings.colorSet.feedbackBackground,
+ ),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth: formWidth,
+ maxHeight: formHeight,
+ minWidth: formWidth,
+ minHeight: formHeight,
+ ),
+ child: RawScrollbar(
+ thumbColor: Settings.colorSet.feedbackScrollbar,
+ interactive: true,
+ controller: _scrollController,
+ thickness: 3,
+ padding: const EdgeInsets.only(top: 5, bottom: -10),
+ thumbVisibility: true,
+ radius: const Radius.circular(2),
+ child: TextField(
+ maxLines: null,
+ scrollController: _scrollController,
+ keyboardType: TextInputType.multiline,
+ style: TextStyle(
+ color: Settings.colorSet.feedbackText,
+ fontSize: SizeConfiguration.feedbackFormFontSize,
+ ),
+ decoration: InputDecoration(
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 10,
+ ),
+ hintText:
+ "Tell us what you think! Anything to improve or add?",
+ hintStyle: TextStyle(
+ color: Settings.colorSet.feedbackHintText,
+ ),
+ border: InputBorder.none,
+ ),
+ onChanged: (final String value) => body = value,
+ ),
+ ),
+ ),
+ ),
+ ),
+ BasicButton(
+ buttonText: "Send Feedback",
+ textColor: Settings.colorSet.text,
+ buttonColor: Settings.colorSet.secondary,
+ width: 200,
+ onPressed: () async {
+ final Uri mailto = generateMailtoURL(
+ MiscellaneousConfiguration.userFeedbackEmailAddress,
+ "Vidar Feedback",
+ body,
+ );
+ try {
+ if (!await launchUrl(mailto)) {
+ if (Settings.keepLogs) {
+ CommonObject.logger!.info(
+ 'Failed to launch mailto: "$mailto"',
+ );
+ }
+ }
+ } catch (error, stackTrace) {
+ if (Settings.keepLogs) {
+ CommonObject.logger!.shout(
+ 'Failed to launch mailto: "$mailto"',
+ error,
+ stackTrace,
+ );
+ }
+ if (context.mounted) {
+ showDialog(
+ context: context,
+ builder: (final BuildContext context) => ErrorPopup(
+ title: "Failed to launch mailto",
+ body: "$error\n$stackTrace\n$mailto",
+ enableReturn: true,
+ ),
+ );
+ }
+ }
+ },
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart
index 10fbc7d..3a1c932 100644
--- a/lib/pages/settings_page.dart
+++ b/lib/pages/settings_page.dart
@@ -32,6 +32,8 @@ class _SettingsPageState extends State {
);
}
+ final ScrollController _scrollController = ScrollController();
+
late final BooleanSetting allowWipeoutTime;
bool allowWipeoutTimeValue = Settings.allowWipeoutTime;
@@ -56,6 +58,16 @@ class _SettingsPageState extends State {
settingText: "Show message bar hints",
);
+ final BooleanSetting allowUserFeedbackDialog = BooleanSetting(
+ setting: Settings.allowUserFeedbackDialog,
+ settingText: "Allow feedback popups",
+ );
+
+ final BooleanSetting use12HourClock = BooleanSetting(
+ setting: Settings.use12HourClock,
+ settingText: "Use 12-hour clock",
+ );
+
final ColorSetSelect colorSetSelect = ColorSetSelect(
selectedSet: Settings.colorSet.name,
);
@@ -78,6 +90,8 @@ class _SettingsPageState extends State {
showEncryptionKeyInEditContact.setting;
Settings.allowUnencryptedMessages = allowUnencryptedMessages.setting;
Settings.allowWipeoutTime = allowWipeoutTimeValue;
+ Settings.allowUserFeedbackDialog = allowUserFeedbackDialog.setting;
+ Settings.use12HourClock = use12HourClock.setting;
if (allowWipeoutTimeValue) {
if (wipeoutTime.setting < 1) {
Settings.allowWipeoutTime = false;
@@ -98,18 +112,29 @@ class _SettingsPageState extends State {
showDialog(
context: context,
builder: (final BuildContext context) => AlertDialog(
- title: const Text("Can't keep logs"),
- content: const Text(
+ title: Text(
+ "Can't keep logs",
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
+ content: Text(
"To keep logs you must allow Vidar to manage external storage.",
+ style: TextStyle(color: Settings.colorSet.dialogText),
),
actions: [
TextButton(
onPressed: () {
clearNavigatorAndPush(context, const ContactListPage());
},
- child: const Text("Continue"),
+ child: Text(
+ "Continue",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
),
],
+ backgroundColor: Settings.colorSet.dialogBackground,
),
);
}
@@ -139,114 +164,157 @@ class _SettingsPageState extends State {
Widget build(final BuildContext context) {
return ColoredBox(
color: Settings.colorSet.primary,
- child: ListView(
- children: [
- Column(
- spacing: 20,
- children: [
- Column(
+ child: LayoutBuilder(
+ builder: (final BuildContext context, final BoxConstraints constraints) {
+ return ConstrainedBox(
+ constraints: constraints,
+ child: RawScrollbar(
+ controller: _scrollController,
+ thumbColor: Settings.colorSet.secondary,
+ thickness: 2,
+ radius: const Radius.circular(1),
+ padding: const EdgeInsets.symmetric(vertical: -10),
+ child: ListView(
+ controller: _scrollController,
children: [
- allowUnencryptedMessages,
- keepLogs,
- showEncryptionKeyInEditContact,
Column(
+ spacing: 20,
children: [
- allowWipeoutTime,
- Visibility(
- visible: allowWipeoutTimeValue,
- child: wipeoutTime,
+ Column(
+ children: [
+ allowUnencryptedMessages,
+ showEncryptionKeyInEditContact,
+ Column(
+ children: [
+ allowWipeoutTime,
+ Visibility(
+ visible: allowWipeoutTimeValue,
+ child: wipeoutTime,
+ ),
+ ],
+ ),
+ keepLogs,
+ allowUserFeedbackDialog,
+ use12HourClock,
+ showMessageBarHints,
+ colorSetSelect,
+ ],
+ ),
+ Container(
+ margin: const EdgeInsets.only(top: 40),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ BasicButton(
+ buttonText: "Discard",
+ textColor: Settings.colorSet.text,
+ buttonColor: Settings.colorSet.secondary,
+ onPressed: _discard,
+ ),
+ BasicButton(
+ buttonText: "Save",
+ textColor: Settings.colorSet.text,
+ buttonColor: Settings.colorSet.tertiary,
+ onPressed: _save,
+ fontWeight: FontWeight.bold,
+ ),
+ ],
+ ),
),
],
),
- showMessageBarHints,
- colorSetSelect,
- ],
- ),
- Container(
- margin: const EdgeInsets.only(top: 40),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- BasicButton(
- buttonText: "Discard",
- textColor: Settings.colorSet.text,
- buttonColor: Settings.colorSet.secondary,
- onPressed: _discard,
- ),
- BasicButton(
- buttonText: "Save",
- textColor: Settings.colorSet.text,
- buttonColor: Settings.colorSet.tertiary,
- onPressed: _save,
- fontWeight: FontWeight.bold,
- ),
- ],
- ),
- ),
- ],
- ),
- Padding(
- padding: const EdgeInsets.only(top: 60),
- child: Column(
- spacing: 50,
- children: [
- Visibility(
- visible: Settings.keepLogs,
- child: BasicButton(
- buttonText: "Export Logs",
- textColor: Settings.colorSet.text,
- buttonColor: Settings.colorSet.exportLogsButton,
- onPressed: () => exportLogs(context: context),
- width: 200,
- ),
- ),
- BasicButton(
- buttonText: "Wipe Keys",
- textColor: Settings.colorSet.wipeKeyButtonText,
- buttonColor: Settings.colorSet.wipeKeyButton,
- width: 200,
- onPressed: () {
- showDialog(
- context: context,
- builder: (final BuildContext context) {
- return AlertDialog(
- title: const Text("Wipe all keys"),
- content: const Text(
- "Are you sure you want to wipe all keys? This is a permanent action which can not be undone.",
+ Padding(
+ padding: const EdgeInsets.only(top: 60),
+ child: Column(
+ spacing: 50,
+ children: [
+ Visibility(
+ visible: Settings.keepLogs,
+ child: BasicButton(
+ buttonText: "Export Logs",
+ textColor: Settings.colorSet.text,
+ buttonColor: Settings.colorSet.exportLogsButton,
+ onPressed: () => exportLogs(context: context),
+ width: 200,
),
- actions: [
- TextButton(
- onPressed: () {
- Navigator.of(context).pop();
- },
- child: const Text("Cancel"),
- ),
- TextButton(
- onPressed: () {
- if (Settings.keepLogs) {
- CommonObject.logger!.info(
- "Wiping all keys...",
- );
- }
- CommonObject.contactList.wipeKeys();
- wipeSecureStorage();
- clearNavigatorAndPush(
- context,
- const ContactListPage(),
+ ),
+ BasicButton(
+ buttonText: "Wipe Keys",
+ textColor: Settings.colorSet.wipeKeyButtonText,
+ buttonColor: Settings.colorSet.wipeKeyButton,
+ width: 200,
+ onPressed: () {
+ showDialog(
+ context: context,
+ builder: (final BuildContext context) {
+ return AlertDialog(
+ title: Text(
+ "Wipe all keys",
+ style: TextStyle(
+ color: Settings.colorSet.dialogText,
+ ),
+ ),
+ content: Text(
+ "Are you sure you want to wipe all keys? This is a permanent action which can not be undone.",
+ style: TextStyle(
+ color: Settings.colorSet.dialogText,
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: Text(
+ "Cancel",
+ style: TextStyle(
+ color: Settings
+ .colorSet
+ .dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ if (Settings.keepLogs) {
+ CommonObject.logger!.info(
+ "Wiping all keys...",
+ );
+ }
+ CommonObject.contactList.wipeKeys();
+ wipeSecureStorage();
+ clearNavigatorAndPush(
+ context,
+ const ContactListPage(),
+ );
+ },
+ child: Text(
+ "Wipe Keys",
+ style: TextStyle(
+ color: Settings
+ .colorSet
+ .dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ ],
+ backgroundColor:
+ Settings.colorSet.dialogBackground,
);
},
- child: const Text("Wipe Keys"),
- ),
- ],
- );
- },
- );
- },
- ),
- ],
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
),
- ),
- ],
+ );
+ },
),
);
}
diff --git a/lib/utils/colors.dart b/lib/utils/colors.dart
index aad3735..6814f4b 100644
--- a/lib/utils/colors.dart
+++ b/lib/utils/colors.dart
@@ -15,6 +15,16 @@ class ColorSet {
final Color? pWipeKeyButtonText,
final Color? pMessageBarHintText,
final Color? pExportLogsButton,
+ final Color? pFeedbackHintText,
+ final Color? pFeedbackBackground,
+ final Color? pDialogButtonText,
+ final Color? pDialogText,
+ final Color? pDialogBackground,
+ final Color? pFeedbackScrollbar,
+ final Color? pDialogScrollbar,
+ final Color? pMessageBarScrollbar,
+ final Color? pFeedbackText,
+ final Color? pIntSettingFill,
}) {
wipeKeyButton = pWipeKeyButton ?? secondary;
inactiveTrack = pInactiveTrack ?? primary;
@@ -24,6 +34,16 @@ class ColorSet {
wipeKeyButtonText = pWipeKeyButtonText ?? text;
messageBarHintText = pMessageBarHintText ?? text;
exportLogsButton = pExportLogsButton ?? tertiary;
+ feedbackHintText = pFeedbackHintText ?? secondary;
+ feedbackBackground = pFeedbackBackground ?? primary;
+ dialogButtonText = pDialogButtonText ?? secondary;
+ dialogText = pDialogText ?? text;
+ dialogBackground = pDialogBackground ?? tertiary;
+ feedbackScrollbar = pFeedbackScrollbar ?? tertiary;
+ dialogScrollbar = pDialogScrollbar ?? tertiary;
+ messageBarScrollbar = pMessageBarScrollbar ?? tertiary;
+ feedbackText = pFeedbackText ?? text;
+ intSettingFill = pIntSettingFill ?? secondary;
}
final String name;
final Color primary;
@@ -39,6 +59,16 @@ class ColorSet {
late final Color wipeKeyButtonText;
late final Color messageBarHintText;
late final Color exportLogsButton;
+ late final Color feedbackHintText;
+ late final Color feedbackBackground;
+ late final Color dialogButtonText;
+ late final Color dialogText;
+ late final Color dialogBackground;
+ late final Color feedbackScrollbar;
+ late final Color dialogScrollbar;
+ late final Color messageBarScrollbar;
+ late final Color feedbackText;
+ late final Color intSettingFill;
}
final List availableColorSets = [
@@ -58,6 +88,15 @@ final ColorSet vidarColorSet = ColorSet(
pFloatingActionButton: const Color.fromARGB(255, 39, 8, 86),
pMessageBarHintText: const Color.fromARGB(255, 172, 116, 255),
pSendButton: const Color.fromARGB(255, 172, 116, 255),
+ pFeedbackHintText: const Color.fromARGB(255, 109, 71, 166),
+ pFeedbackBackground: const Color.fromARGB(255, 30, 32, 45),
+ pDialogBackground: const Color.fromARGB(255, 172, 116, 255),
+ pDialogText: const Color.fromARGB(255, 53, 22, 100),
+ pDialogButtonText: const Color.fromARGB(255, 26, 28, 40),
+ pFeedbackScrollbar: const Color.fromARGB(255, 109, 71, 166),
+ pDialogScrollbar: const Color.fromARGB(255, 109, 71, 166),
+ pMessageBarScrollbar: const Color.fromARGB(255, 172, 116, 255),
+ pIntSettingFill: const Color.fromARGB(255, 33, 36, 52),
);
final ColorSet playaColorSet = ColorSet(
@@ -68,6 +107,13 @@ final ColorSet playaColorSet = ColorSet(
text: const Color.fromARGB(255, 28, 60, 103),
pWipeKeyButton: const Color.fromARGB(255, 255, 125, 41),
pInactiveTrack: const Color.fromARGB(255, 250, 218, 122),
+ pFeedbackBackground: const Color.fromARGB(255, 28, 60, 103),
+ pFeedbackHintText: const Color.fromARGB(255, 78, 215, 241),
+ pFeedbackScrollbar: const Color.fromARGB(255, 49, 203, 234),
+ pFeedbackText: const Color.fromARGB(255, 168, 241, 255),
+ pDialogButtonText: const Color.fromARGB(255, 250, 218, 122),
+ pDialogText: const Color.fromARGB(255, 255, 250, 141),
+ pDialogScrollbar: const Color.fromARGB(255, 255, 250, 141),
);
final ColorSet monochromeColorSet = ColorSet(
@@ -80,6 +126,10 @@ final ColorSet monochromeColorSet = ColorSet(
pInactiveTrack: const Color.fromARGB(255, 41, 41, 41),
pSendButton: const Color.fromARGB(255, 200, 200, 200),
pWipeKeyButtonText: Colors.black,
+ pFeedbackScrollbar: const Color.fromARGB(255, 63, 63, 63),
+ pDialogText: const Color.fromARGB(255, 147, 147, 147),
+ pDialogButtonText: const Color.fromARGB(255, 200, 200, 200),
+ pFeedbackHintText: const Color.fromARGB(255, 147, 147, 147),
);
final ColorSet bubblyColorSet = ColorSet(
@@ -94,6 +144,8 @@ final ColorSet bubblyColorSet = ColorSet(
pDropdownFocus: const Color.fromARGB(255, 165, 40, 255),
pMessageBarHintText: const Color.fromARGB(255, 66, 191, 221),
pExportLogsButton: const Color.fromARGB(255, 165, 40, 255),
+ pFeedbackScrollbar: const Color.fromARGB(255, 165, 40, 255),
+ pIntSettingFill: const Color.fromARGB(255, 71, 32, 163),
);
/// If color set is not found then it returns default
diff --git a/lib/utils/conversation.dart b/lib/utils/conversation.dart
index 2b8694e..fc320df 100644
--- a/lib/utils/conversation.dart
+++ b/lib/utils/conversation.dart
@@ -1,64 +1,129 @@
-import "package:flutter/material.dart";
+import "package:cryptography/cryptography.dart";
import "package:vidar/configuration.dart";
import "package:vidar/utils/common_object.dart";
import "package:vidar/utils/contact.dart";
+import "package:vidar/utils/encryption.dart";
+import "package:vidar/utils/extended_change_notifier.dart";
import "package:vidar/utils/settings.dart";
import "package:vidar/utils/sms.dart";
-import "package:vidar/widgets/error_popup.dart";
-class Conversation extends ChangeNotifier {
+class Conversation extends ExtendedChangeNotifier {
Conversation(this.contact) {
smsNotifier = SmsNotifier();
- smsNotifier.addListener(notifyListeners);
+ attach(smsNotifier);
}
final Contact contact;
- late List chatLogs;
+ late final List _decryptedChatLogs;
+ // If there are a lot of chat logs then this can improve performance
+ // by not requiring a complete count (e.g. +10K messages, etc)
+ int _currentChatLogsLength = 0;
late final SmsNotifier smsNotifier;
- Future updateChatLogs({final BuildContext? context}) async {
- final List updatedChatLogs = (await querySms(
+ List get decryptedChatLogs => _decryptedChatLogs;
+ int get chatLogsLength => _currentChatLogsLength;
+
+ /// This queries all chat logs from contact, use should be minimized. Instead use updateChatLogs
+ Future queryChatLogs() async {
+ final List chatLogs = await querySms(
phoneNumber: contact.phoneNumber,
- )).toList();
- if (updatedChatLogs[0] == null) {
- // this is a stupid operation since there are no null elements but it makes the compiler shut up
- chatLogs = updatedChatLogs.whereType().toList();
- } else {
- if (context != null && context.mounted) {
- showDialog(
- context: context,
- builder: (final BuildContext context) => const ErrorPopup(
- title: "Failed to update chat logs",
- body: "updatedChatLogs=[null]",
- enableReturn: false,
- ),
- );
- }
+ );
+
+ if (chatLogs[0] == null) {
if (Settings.keepLogs) {
- CommonObject.logger!.info("Failed to update chat logs");
+ CommonObject.logger!.info(
+ "Failed to query chat logs for contact ${contact.uuid}",
+ );
}
+ return ConversationStatus.FAILURE;
}
- if (LoggingConfiguration.extraVerboseLogs && Settings.keepLogs) {
- CommonObject.logger!.info(
- "Chat logs updated for contact ${contact.uuid}",
- );
+
+ // Maybe in the future SMS messages will be mutable and not require cloning but for now,
+ // I'm unsure if i should do it.
+ // So for the moment it still clones.
+ _currentChatLogsLength = chatLogs.length;
+ _decryptedChatLogs = [];
+ for (final SmsMessage chat in chatLogs as List) {
+ if (chat.status == SmsConstants.STATUS_FAILED) {
+ _decryptedChatLogs.add(chat.clone(newBody: "MESSAGE_FAILED"));
+ } else {
+ _decryptedChatLogs.add(
+ chat.clone(
+ newBody: await decryptMessage(
+ chat.body,
+ contact.encryptionKey,
+ algorithm: AesGcm.with256bits(
+ nonceLength: CryptographicConfiguration.nonceLength,
+ ),
+ ),
+ ),
+ );
+ }
}
- notifyListeners();
+ return ConversationStatus.SUCCESS;
}
- Future closeConversation() async {
- smsNotifier.removeListener(notifyListeners);
- }
+ /// Only checks "latestN" number of chats from contact to update
+ /// Returns true on success
+ Future updateChatLogs({required int latestN}) async {
+ if (_currentChatLogsLength == 0) {
+ return queryChatLogs();
+ }
+ if (latestN > _currentChatLogsLength) {
+ latestN = _currentChatLogsLength;
+ }
- void externalNotify() {
- if (LoggingConfiguration.extraVerboseLogs && Settings.keepLogs) {
- CommonObject.logger!.info("External notify for contact ${contact.uuid}");
+ final List chatLogs = await querySms(
+ phoneNumber: contact.phoneNumber,
+ latestN: latestN,
+ );
+
+ if (chatLogs[0] == null) {
+ if (Settings.keepLogs) {
+ CommonObject.logger!.info(
+ "Failed to query chat logs for contact ${contact.uuid}",
+ );
+ }
+ return ConversationStatus.FAILURE;
+ }
+
+ // Maybe in the future SMS messages will be mutable and not require cloning but for now,
+ // I'm unsure if i should do it.
+ // So for the moment it still clones.
+ final List latestMessageDates = [];
+ for (final SmsMessage? chat in decryptedChatLogs.sublist(
+ 0,
+ ChatConfiguration.numCheckDuringUpdate,
+ )) {
+ latestMessageDates.add(chat!.date!);
}
- notifyListeners();
- }
- @override
- void notifyListeners() {
- super.notifyListeners();
+ for (final SmsMessage chat in chatLogs as List) {
+ // Checks to avoid duplicates
+ if (latestMessageDates.contains(chat.date)) {
+ continue;
+ } else {
+ ++_currentChatLogsLength;
+ }
+ if (chat.status == SmsConstants.STATUS_FAILED) {
+ _decryptedChatLogs.insert(0, chat.clone(newBody: "MESSAGE_FAILED"));
+ } else {
+ _decryptedChatLogs.insert(
+ 0,
+ chat.clone(
+ newBody: await decryptMessage(
+ chat.body,
+ contact.encryptionKey,
+ algorithm: AesGcm.with256bits(
+ nonceLength: CryptographicConfiguration.nonceLength,
+ ),
+ ),
+ ),
+ );
+ }
+ }
+ return ConversationStatus.SUCCESS;
}
}
+
+enum ConversationStatus { FAILURE, SUCCESS }
diff --git a/lib/utils/encryption.dart b/lib/utils/encryption.dart
index c9738c0..fb75d26 100644
--- a/lib/utils/encryption.dart
+++ b/lib/utils/encryption.dart
@@ -23,7 +23,7 @@ Future encryptMessage(final String message, final String key) async {
if (Settings.allowUnencryptedMessages) {
return message;
} else {
- return "${MiscellaneousConfiguration.errorPrefix}$ENCRYPTION_ERROR_NO_KEY";
+ return "${ChatConfiguration.errorPrefix}$ENCRYPTION_ERROR_NO_KEY";
}
}
@@ -58,7 +58,7 @@ Future encryptMessage(final String message, final String key) async {
stackTrace,
);
}
- return "${MiscellaneousConfiguration.errorPrefix}$ENCRYPTION_ERROR_ENCRYPTION_FAILED";
+ return "${ChatConfiguration.errorPrefix}$ENCRYPTION_ERROR_ENCRYPTION_FAILED";
}
}
@@ -123,7 +123,7 @@ Future decryptMessage(
if (Settings.keepLogs) {
CommonObject.logger!.warning("Failed to decrypt message", error);
}
- return "${MiscellaneousConfiguration.errorPrefix}$ENCRYPTION_ERROR_DECRYPTION_FAILED";
+ return "${ChatConfiguration.errorPrefix}$ENCRYPTION_ERROR_DECRYPTION_FAILED";
} catch (error, stackTrace) {
if (Settings.keepLogs) {
CommonObject.logger!.finer(
@@ -132,6 +132,6 @@ Future decryptMessage(
stackTrace,
);
}
- return "${MiscellaneousConfiguration.errorPrefix}$ENCRYPTION_ERROR_DECRYPTION_FAILED";
+ return "${ChatConfiguration.errorPrefix}$ENCRYPTION_ERROR_DECRYPTION_FAILED";
}
}
diff --git a/lib/utils/extended_change_notifier.dart b/lib/utils/extended_change_notifier.dart
new file mode 100644
index 0000000..a710c8d
--- /dev/null
+++ b/lib/utils/extended_change_notifier.dart
@@ -0,0 +1,26 @@
+import "package:flutter/material.dart";
+
+class ExtendedChangeNotifier extends ChangeNotifier {
+ final List _attachedNotifiers = [];
+
+ @override
+ void notifyListeners() {
+ super.notifyListeners();
+ }
+
+ /// Attaches this change notifier to another.
+ /// If the attaché notifies its listeners then this change notifiers also notifies its listeners.
+ /// This change notifier removes itself from the attaché when being disposed of.
+ void attach(final ChangeNotifier changeNotifier) {
+ changeNotifier.addListener(notifyListeners);
+ _attachedNotifiers.add(changeNotifier);
+ }
+
+ @override
+ void dispose() {
+ for (final ChangeNotifier notifier in _attachedNotifiers) {
+ notifier.removeListener(notifyListeners);
+ }
+ super.dispose();
+ }
+}
diff --git a/lib/utils/log.dart b/lib/utils/log.dart
index f5002b5..c2abffc 100644
--- a/lib/utils/log.dart
+++ b/lib/utils/log.dart
@@ -86,15 +86,28 @@ Future exportLogs({final BuildContext? context}) async {
showDialog(
context: context,
builder: (final BuildContext context) => AlertDialog(
- title: const Text("Logs exported"),
- content: Text('Logs have been exported to "${file.path}"'),
+ title: Text(
+ "Logs exported",
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
+ content: Text(
+ 'Logs have been exported to "${file.path}"',
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
actions: [
TextButton(
onPressed: () =>
clearNavigatorAndPush(context, const ContactListPage()),
- child: const Text("Dismiss"),
+ child: Text(
+ "Dismiss",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
),
],
+ backgroundColor: Settings.colorSet.dialogBackground,
),
);
}
diff --git a/lib/utils/public_change_notifier.dart b/lib/utils/public_change_notifier.dart
deleted file mode 100644
index b06d601..0000000
--- a/lib/utils/public_change_notifier.dart
+++ /dev/null
@@ -1,8 +0,0 @@
-import "package:flutter/material.dart";
-
-class PublicChangeNotifier extends ChangeNotifier {
- @override
- void notifyListeners() {
- super.notifyListeners();
- }
-}
diff --git a/lib/utils/settings.dart b/lib/utils/settings.dart
index c5c4c50..4caffef 100644
--- a/lib/utils/settings.dart
+++ b/lib/utils/settings.dart
@@ -2,6 +2,7 @@ import "package:vidar/utils/colors.dart";
/// Static class for storing the active user settings of the program.
class Settings {
+ // The init value is the default value for each setting
/// Send unencrypted messages when contact has no key.
static bool allowUnencryptedMessages = false;
@@ -17,6 +18,10 @@ class Settings {
static ColorSet colorSet = vidarColorSet;
+ static bool allowUserFeedbackDialog = true;
+
+ static bool use12HourClock = false;
+
/// Get map of the state of all instance variable of Settings.
static Map toMap() {
return {
@@ -27,6 +32,8 @@ class Settings {
"wipeoutTime": wipeoutTime,
"showMessageBarHints": showMessageBarHints,
"colorSet": colorSet.name,
+ "allowUserFeedbackDialog": allowUserFeedbackDialog,
+ "use12HourClock": use12HourClock,
};
}
@@ -42,8 +49,10 @@ class Settings {
allowWipeoutTime = map["allowWipeoutTime"] as bool? ?? allowWipeoutTime;
wipeoutTime = map["wipeoutTime"] as int? ?? wipeoutTime;
colorSet = getColorSetFromName(map["colorSet"] as String? ?? "default");
-
allowUnencryptedMessages =
map["allowUnencryptedMessages"]! as bool? ?? allowUnencryptedMessages;
+ allowUserFeedbackDialog =
+ map["allowUserFeedbackDialog"] as bool? ?? allowUserFeedbackDialog;
+ use12HourClock = map["use12HourClock"] as bool? ?? use12HourClock;
}
}
diff --git a/lib/utils/sms.dart b/lib/utils/sms.dart
index 931680e..b836393 100644
--- a/lib/utils/sms.dart
+++ b/lib/utils/sms.dart
@@ -95,9 +95,10 @@ class SmsMessage {
}
/// Requires an initialization of SmsConstants beforehand
-SmsMessage? _queryMapToSms(final Map smsMap) {
+/// Throws an exception if SmsConstants.mapConstants == null
+SmsMessage _queryMapToSms(final Map smsMap) {
if (SmsConstants.mapConstants == null) {
- return null;
+ throw Exception("SmsConstants.mapConstants == null");
}
final int? threadId = int.tryParse(
smsMap[SmsConstants.COLUMN_NAME_THREAD_ID]!,
@@ -142,11 +143,14 @@ SmsMessage? _queryMapToSms(final Map smsMap) {
/// Requires an initialization of SmsConstants beforehand
/// SMS are returned oldest to newest
/// Returns [null] upon failure (this is to ensure compatibility with FutureBuilder)
-Future> querySms({final String? phoneNumber}) async {
+Future> querySms({
+ final String? phoneNumber,
+ final int? latestN,
+}) async {
try {
final dynamic rawResult = await MAIN_SMS_CHANNEL.invokeMethod(
"querySms",
- {"phoneNumber": phoneNumber},
+ {"phoneNumber": phoneNumber, "latestN": latestN},
);
if (rawResult == null) {
if (LoggingConfiguration.extraVerboseLogs && Settings.keepLogs) {
@@ -161,8 +165,19 @@ Future> querySms({final String? phoneNumber}) async {
);
}
final List smsMessages = [];
- for (final Map mapMessage in result) {
- smsMessages.add(_queryMapToSms(mapMessage)!);
+ try {
+ for (final Map mapMessage in result) {
+ smsMessages.add(_queryMapToSms(mapMessage));
+ }
+ } catch (error, stackTrace) {
+ if (Settings.keepLogs) {
+ CommonObject.logger!.warning(
+ "Failed to convert sms maps to SmsMessage",
+ error,
+ stackTrace,
+ );
+ }
+ return [null];
}
if (smsMessages.isEmpty) {
if (LoggingConfiguration.extraVerboseLogs && Settings.keepLogs) {
@@ -172,7 +187,7 @@ Future> querySms({final String? phoneNumber}) async {
return smsMessages;
} on PlatformException catch (error, stackTrace) {
if (Settings.keepLogs) {
- CommonObject.logger!.finer("Sms query failed", error, stackTrace);
+ CommonObject.logger!.finest("Sms query failed", error, stackTrace);
}
return [null];
}
@@ -180,10 +195,11 @@ Future> querySms({final String? phoneNumber}) async {
/// The phone number is that of the other party
Future sendSms(final String body, final String phoneNumber) async {
- /// 0 = success, for now not used
if (LoggingConfiguration.extraVerboseLogs && Settings.keepLogs) {
CommonObject.logger!.info("Sending sms");
}
+
+ // 0 = success, for now not used
// ignore: unused_local_variable
final dynamic result = await MAIN_SMS_CHANNEL.invokeMethod(
"sendSms",
@@ -274,18 +290,23 @@ class SmsConstants {
static late final String COLUMN_NAME_BODY;
}
-/// Notifies when (any) sms is recieved
+/// Notifies when (any) sms is received
class SmsNotifier extends ChangeNotifier {
SmsNotifier() {
SMS_NOTIFIER_CHANNEL.receiveBroadcastStream().listen((
final dynamic onData,
) {
if (onData is String && onData == "smsreceived") {
- notifyListeners();
+ // This delay is required for the notifier to work propely with incoming SMS
+ // If there is not delay then it will rebuild before the the new message can be reached
+ // via query.
+ Future.delayed(
+ const Duration(seconds: 1),
+ ).then((_) => notifyListeners());
}
});
}
- // if later you can choose the specific phone number then this can't be static
+ // If later you can choose the specific phone number then this can't be static
static const EventChannel SMS_NOTIFIER_CHANNEL = EventChannel(
"flutter.native/smsnotifier",
);
diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart
index b93d7de..1e35109 100644
--- a/lib/utils/storage.dart
+++ b/lib/utils/storage.dart
@@ -123,6 +123,7 @@ Show message bar hints: ${Settings.showMessageBarHints}
Show encryption key in edit contact: ${Settings.showEncryptionKeyInEditContact}
Allow wipeout: ${Settings.allowWipeoutTime}
Wipeout time: ${Settings.wipeoutTime} days
+Allow user feedback dialog: ${Settings.allowUserFeedbackDialog}
""");
}
} on Exception catch (error, stackTrace) {
diff --git a/lib/utils/time.dart b/lib/utils/time.dart
new file mode 100644
index 0000000..0c43a4e
--- /dev/null
+++ b/lib/utils/time.dart
@@ -0,0 +1,17 @@
+extension TimeFormatting on DateTime {
+ String format24HourTime() {
+ return "$hour:$minute";
+ }
+
+ // If time is
+ String format12HourTime() {
+ final bool isAM = hour < 12;
+ int h = hour;
+ if (!isAM) {
+ h -= 12;
+ } else if (h == 0) {
+ h = 12;
+ }
+ return "$h:$minute ${isAM ? "a.m." : "p.m."}";
+ }
+}
diff --git a/lib/utils/url.dart b/lib/utils/url.dart
new file mode 100644
index 0000000..71ef6a2
--- /dev/null
+++ b/lib/utils/url.dart
@@ -0,0 +1,33 @@
+Uri generateMailtoURL(
+ final String address,
+ final String subject,
+ final String body,
+) {
+ return Uri.parse(
+ "mailto:$address?subject=${encodeStringToUri(subject)}&body=${encodeStringToUri(body)}",
+ );
+}
+
+Uri encodeStringToUri(final String str) {
+ return Uri.parse(
+ str
+ .replaceAll("!", "%21")
+ .replaceAll("#", "%23")
+ .replaceAll(r"$", "%24")
+ .replaceAll("&", "%26")
+ .replaceAll("'", "%27")
+ .replaceAll("(", "%28")
+ .replaceAll(")", "%29")
+ .replaceAll("*", "%2A")
+ .replaceAll("+", "%2B")
+ .replaceAll(",", "%2C")
+ .replaceAll("/", "%2F")
+ .replaceAll(":", "%3A")
+ .replaceAll(";", "%3B")
+ .replaceAll("=", "%3D")
+ .replaceAll("?", "%3F")
+ .replaceAll("@", "%40")
+ .replaceAll("[", "%5B")
+ .replaceAll("]", "%5D"),
+ );
+}
diff --git a/lib/widgets/contact_badge.dart b/lib/widgets/contact_badge.dart
index 1544eca..c2c4519 100644
--- a/lib/widgets/contact_badge.dart
+++ b/lib/widgets/contact_badge.dart
@@ -58,16 +58,26 @@ class ContactBadge extends StatelessWidget {
const ContactListPage(),
Center(
child: AlertDialog(
- title: const Text("Delete contact"),
+ title: Text(
+ "Delete contact",
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
content: Text(
'Are you sure you want to delete "${contact.name}?"',
+ style: TextStyle(color: Settings.colorSet.dialogText),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
- child: const Text("Back"),
+ child: Text(
+ "Back",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
),
TextButton(
onPressed: () {
@@ -87,9 +97,16 @@ class ContactBadge extends StatelessWidget {
const ContactListPage(),
);
},
- child: const Text("Delete"),
+ child: Text(
+ "Delete",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
),
],
+ backgroundColor: Settings.colorSet.dialogBackground,
),
),
],
diff --git a/lib/widgets/conversation_widget.dart b/lib/widgets/conversation_widget.dart
index 899e78f..9aa7ecb 100644
--- a/lib/widgets/conversation_widget.dart
+++ b/lib/widgets/conversation_widget.dart
@@ -1,12 +1,10 @@
-import "package:cryptography/cryptography.dart" show AesGcm;
import "package:flutter/material.dart";
import "package:vidar/configuration.dart";
import "package:vidar/utils/common_object.dart";
import "package:vidar/utils/contact.dart";
import "package:vidar/utils/conversation.dart";
-import "package:vidar/utils/encryption.dart";
import "package:vidar/utils/settings.dart";
-import "package:vidar/utils/sms.dart";
+import "package:vidar/widgets/info_text_widget.dart";
import "package:vidar/widgets/loading_screen.dart";
import "package:vidar/widgets/speech_bubble.dart";
@@ -21,7 +19,6 @@ class ConversationWidget extends StatefulWidget {
class _ConversationWidgetState extends State {
_ConversationWidgetState();
late Contact contact;
- late Conversation conversation;
bool chatLoaded = false;
String loadMessage = "Loading...";
@@ -29,8 +26,13 @@ class _ConversationWidgetState extends State {
void initState() {
super.initState();
contact = widget.contact;
- conversation = Conversation(contact);
- CommonObject.currentConversation = conversation;
+ CommonObject.currentConversation = Conversation(contact);
+ }
+
+ @override
+ void dispose() {
+ CommonObject.currentConversation?.dispose();
+ super.dispose();
}
@override
@@ -39,108 +41,67 @@ class _ConversationWidgetState extends State {
CommonObject.logger!.info("Querying sms for contact ${contact.uuid}...");
}
return ListenableBuilder(
- listenable: conversation,
- builder: (final BuildContext context, final Widget? asyncSnapshot) {
- final Future?> smsFuture = Future.delayed(
- const Duration(seconds: 1),
- ).then((final void _) => querySms(phoneNumber: contact.phoneNumber));
-
- return FutureBuilder?>(
- future: smsFuture,
+ listenable: CommonObject.currentConversation!,
+ builder: (final BuildContext context, final Widget? child) {
+ return FutureBuilder(
+ future: CommonObject.currentConversation!.updateChatLogs(latestN: 5),
builder:
(
final BuildContext context,
- final AsyncSnapshot?> snapshot,
+ final AsyncSnapshot snapshot,
) {
if (!snapshot.hasData) {
return ChatLoadingScreen(contact.name);
} else {
// snapshot.data == [null] does not work
- if (snapshot.data![0] == null) {
- if (Settings.keepLogs) {
- CommonObject.logger!.info(
- "SMS query failed for contact ${contact.uuid}",
- );
- }
- return ColoredBox(
- color: Settings.colorSet.primary,
- child: Center(
- child: SizedBox(
- width: MediaQuery.of(context).size.width * 0.6,
- child: Center(
- child: Text(
- "SMS query failed, please ensure the phone number is correct.",
- style: TextStyle(
- fontSize: 20,
- color: Settings.colorSet.text,
- ),
- ),
- ),
- ),
- ),
- );
- } else {
- final List chatLogs = snapshot.data!
- .whereType()
- .toList();
- conversation.chatLogs = [];
- for (final SmsMessage chat in chatLogs) {
- if (chat.status == SmsConstants.STATUS_FAILED) {
- conversation.chatLogs.add(
- chat.clone(newBody: "MESSAGE_FAILED"),
+ switch (snapshot.data) {
+ case ConversationStatus.FAILURE:
+ if (Settings.keepLogs) {
+ CommonObject.logger!.info(
+ "SMS query failed for contact ${contact.uuid}",
);
- } else {
- conversation.chatLogs.add(chat);
}
- }
- if (LoggingConfiguration.extraVerboseLogs &&
- Settings.keepLogs) {
- CommonObject.logger!.info(
- "SMS query complete for contact ${contact.uuid}",
+ return const InfoText(
+ text:
+ "SMS query failed, please ensure the phone number is correct.",
+ textWidthFactor: 0.8,
+ fontSize: 20,
);
- }
- final List decryptedSpeechBubbles = [];
- for (final SmsMessage message in conversation.chatLogs) {
- decryptedSpeechBubbles.add(
- FutureBuilder(
- future: decryptMessage(
- message.body,
- contact.encryptionKey,
- algorithm: AesGcm.with256bits(
- nonceLength:
- CryptographicConfiguration.nonceLength,
- ),
- ),
- builder:
- (
- final BuildContext context,
- final AsyncSnapshot snapshot,
- ) {
- if (snapshot.hasData) {
- return SpeechBubble(
- message.clone(newBody: snapshot.data),
- );
- } else {
- if (LoggingConfiguration.extraVerboseLogs &&
- Settings.keepLogs) {
- CommonObject.logger!.info(
- "Snapshot has no data for contact ${contact.uuid}",
- );
- }
- }
- return const SizedBox.shrink();
- },
+ case ConversationStatus.SUCCESS:
+ if (LoggingConfiguration.extraVerboseLogs &&
+ Settings.keepLogs) {
+ CommonObject.logger!.info(
+ "SMS query complete for contact ${contact.uuid}",
+ );
+ }
+ return ColoredBox(
+ color: Settings.colorSet.primary,
+ child: ListView.builder(
+ reverse: true,
+ itemCount:
+ CommonObject.currentConversation!.chatLogsLength,
+ itemBuilder:
+ (final BuildContext context, final int index) =>
+ SpeechBubble(
+ CommonObject
+ .currentConversation!
+ .decryptedChatLogs[index],
+ ),
),
);
- }
- return ColoredBox(
- color: Settings.colorSet.primary,
- child: ListView(
- reverse: true,
- children: decryptedSpeechBubbles,
- ),
- );
+
+ default:
+ if (Settings.keepLogs) {
+ CommonObject.logger!.info(
+ "Unexpected case (${snapshot.data}) when loading chat logs for contact ${contact.uuid}",
+ );
+ }
+ return InfoText(
+ text:
+ "Unexpected case (${snapshot.data}) when loading chat logs",
+ textWidthFactor: 0.6,
+ );
}
}
},
diff --git a/lib/widgets/error_popup.dart b/lib/widgets/error_popup.dart
index dc5938c..1e11c1c 100644
--- a/lib/widgets/error_popup.dart
+++ b/lib/widgets/error_popup.dart
@@ -32,37 +32,7 @@ class _ErrorPopupState extends State {
late final String title;
late final String body;
late final bool enableReturn;
-
- List actions(final BuildContext context) {
- final List list = [
- TextButton(
- onPressed: () {
- clearNavigatorAndPush(context, const ContactListPage());
- },
- child: const Text("Home"),
- ),
- ];
- if (Settings.keepLogs) {
- list.add(
- TextButton(
- onPressed: () {
- exportLogs();
- clearNavigatorAndPush(context, const ContactListPage());
- },
- child: const Text("Export logs"),
- ),
- );
- }
- list.add(
- TextButton(
- onPressed: () {
- Navigator.of(context).pop();
- },
- child: const Text("Back"),
- ),
- );
- return list;
- }
+ final ScrollController _scrollController = ScrollController();
@override
void initState() {
@@ -80,9 +50,72 @@ class _ErrorPopupState extends State {
const ContactListPage(),
Center(
child: AlertDialog(
- title: Text(title),
- content: Text(body),
- actions: actions(context),
+ title: Text(
+ title,
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
+ content: ConstrainedBox(
+ constraints: BoxConstraints(
+ maxHeight: MediaQuery.of(context).size.height / 2,
+ ),
+ child: RawScrollbar(
+ thumbColor: Settings.colorSet.dialogScrollbar,
+ controller: _scrollController,
+ thumbVisibility: true,
+ thickness: 2,
+ interactive: true,
+ padding: const EdgeInsets.only(left: 4),
+ child: TextField(
+ readOnly: true,
+ scrollController: _scrollController,
+ controller: TextEditingController(text: body),
+ maxLines: null,
+ decoration: const InputDecoration(border: InputBorder.none),
+ style: TextStyle(color: Settings.colorSet.dialogText),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ clearNavigatorAndPush(context, const ContactListPage());
+ },
+ child: Text(
+ "Home",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ if (Settings.keepLogs)
+ TextButton(
+ onPressed: () {
+ exportLogs();
+ clearNavigatorAndPush(context, const ContactListPage());
+ },
+ child: Text(
+ "Export logs",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: Text(
+ "Back",
+ style: TextStyle(
+ color: Settings.colorSet.dialogButtonText,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ ],
+ backgroundColor: Settings.colorSet.dialogBackground,
),
),
],
diff --git a/lib/widgets/info_text_widget.dart b/lib/widgets/info_text_widget.dart
new file mode 100644
index 0000000..f135693
--- /dev/null
+++ b/lib/widgets/info_text_widget.dart
@@ -0,0 +1,36 @@
+import "package:flutter/material.dart";
+import "package:vidar/utils/settings.dart";
+
+class InfoText extends StatelessWidget {
+ const InfoText({
+ required this.text,
+ required this.textWidthFactor,
+ this.fontSize = 20,
+ super.key,
+ });
+
+ final String text;
+ final double textWidthFactor;
+ final double fontSize;
+
+ @override
+ Widget build(final BuildContext context) {
+ return ColoredBox(
+ color: Settings.colorSet.primary,
+ child: Center(
+ child: SizedBox(
+ width: MediaQuery.of(context).size.width * textWidthFactor,
+ child: Text(
+ text,
+ style: TextStyle(
+ color: Settings.colorSet.text,
+ fontSize: fontSize,
+ decoration: TextDecoration.none,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/int_setting.dart b/lib/widgets/int_setting.dart
index 888412f..96170c6 100644
--- a/lib/widgets/int_setting.dart
+++ b/lib/widgets/int_setting.dart
@@ -61,7 +61,8 @@ class _IntSettingState extends State {
),
style: TextStyle(color: Settings.colorSet.text),
decoration: InputDecoration(
- fillColor: Settings.colorSet.secondary,
+ fillColor: Settings.colorSet.intSettingFill,
+ filled: true,
counter: const SizedBox.shrink(),
focusedBorder: OutlineInputBorder(
diff --git a/lib/widgets/loading_screen.dart b/lib/widgets/loading_screen.dart
index 6b85e40..25fc293 100644
--- a/lib/widgets/loading_screen.dart
+++ b/lib/widgets/loading_screen.dart
@@ -44,9 +44,6 @@ class LoadingScreen extends StatelessWidget {
fontSize: SizeConfiguration.loadingFontSize,
),
),
- /*LoadingText(
- style: TextStyle(color: VidarColors.secondaryMetallicViolet, fontSize: SizeConfiguration.loadingFontSize),
- ),*/
],
),
);
diff --git a/lib/widgets/message_bar.dart b/lib/widgets/message_bar.dart
index af8b6ac..dd4ff93 100644
--- a/lib/widgets/message_bar.dart
+++ b/lib/widgets/message_bar.dart
@@ -5,7 +5,7 @@ import "package:vidar/configuration.dart";
import "package:vidar/utils/common_object.dart";
import "package:vidar/utils/contact.dart";
import "package:vidar/utils/encryption.dart";
-import "package:vidar/utils/public_change_notifier.dart";
+import "package:vidar/utils/extended_change_notifier.dart";
import "package:vidar/utils/settings.dart";
import "package:vidar/utils/sms.dart";
@@ -23,8 +23,9 @@ class _MessageBarState extends State {
String message = "";
bool error = false;
String errorMessage = "";
- PublicChangeNotifier errorNotifier = PublicChangeNotifier();
+ ExtendedChangeNotifier errorNotifier = ExtendedChangeNotifier();
final TextEditingController controller = TextEditingController();
+ final ScrollController _scrollController = ScrollController();
@override
void initState() {
@@ -133,29 +134,42 @@ class _MessageBarState extends State {
width:
MediaQuery.sizeOf(context).width -
SizeConfiguration.sendMessageIconSize * 2.5,
- child: TextField(
- controller: controller,
- style: TextStyle(color: Settings.colorSet.text),
- decoration: InputDecoration(
- hintText: () {
- if (Settings.showMessageBarHints) {
- return MiscellaneousConfiguration
- .messageHints[Random().nextInt(
- MiscellaneousConfiguration.messageHints.length,
- )];
- } else {
- return null;
- }
- }(),
- hintStyle: TextStyle(
- color: Settings.colorSet.messageBarHintText,
- ),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(10.0),
- borderSide: BorderSide.none,
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxHeight: 150),
+ child: RawScrollbar(
+ thickness: 2,
+ padding: const EdgeInsets.only(bottom: -30),
+ radius: const Radius.circular(1),
+ controller: _scrollController,
+ thumbColor: Settings.colorSet.messageBarScrollbar,
+ thumbVisibility: true,
+ child: TextField(
+ scrollController: _scrollController,
+ maxLines: null,
+ controller: controller,
+ style: TextStyle(color: Settings.colorSet.text),
+ decoration: InputDecoration(
+ hintText: () {
+ if (Settings.showMessageBarHints) {
+ return ChatConfiguration.messageHints[Random()
+ .nextInt(
+ ChatConfiguration.messageHints.length,
+ )];
+ } else {
+ return null;
+ }
+ }(),
+ hintStyle: TextStyle(
+ color: Settings.colorSet.messageBarHintText,
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10.0),
+ borderSide: BorderSide.none,
+ ),
+ ),
+ onChanged: (final String value) => message = value,
),
),
- onChanged: (final String value) => message = value,
),
),
SizedBox(
@@ -169,10 +183,10 @@ class _MessageBarState extends State {
contact.encryptionKey,
);
if (encryptedMessage.startsWith(
- MiscellaneousConfiguration.errorPrefix,
+ ChatConfiguration.errorPrefix,
)) {
errorMessage = encryptedMessage.replaceFirst(
- MiscellaneousConfiguration.errorPrefix,
+ ChatConfiguration.errorPrefix,
"",
);
error = true;
diff --git a/lib/widgets/speech_bubble.dart b/lib/widgets/speech_bubble.dart
index 123f911..ff42d32 100644
--- a/lib/widgets/speech_bubble.dart
+++ b/lib/widgets/speech_bubble.dart
@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
import "package:vidar/configuration.dart";
import "package:vidar/utils/settings.dart";
import "package:vidar/utils/sms.dart";
+import "package:vidar/utils/time.dart";
class SpeechBubble extends StatelessWidget {
const SpeechBubble(this.message, {super.key});
@@ -54,7 +55,9 @@ class SpeechBubble extends StatelessWidget {
margin: const EdgeInsets.only(top: 2.0, bottom: 2.0),
child: Text(
(isMe ? "Sent at " : "Received at ") +
- message.date!.toIso8601String().substring(11, 16),
+ (Settings.use12HourClock
+ ? message.date!.format12HourTime()
+ : message.date!.format24HourTime()),
style: const TextStyle(color: Colors.white, fontSize: 8),
),
),
diff --git a/pubspec.lock b/pubspec.lock
index bcb9b40..d0ed672 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -432,6 +432,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.2"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.17"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.3"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.2"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.4"
uuid:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index c49366e..3d1a61a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: vidar
description: "Private Communications Application"
publish_to: "none"
-version: 1.2.2-beta+5
+version: 1.3.0-beta+6
repository: "https://github.com/DrSolidDevil/Vidar"
issue_tracker: "https://github.com/DrSolidDevil/Vidar/issues"
@@ -19,6 +19,7 @@ dependencies:
uuid: ^4.5.1
device_info_plus: ^11.5.0
package_info_plus: ^8.3.0
+ url_launcher: ^6.3.2
flutter:
uses-material-design: true
diff --git a/screenshots/chat_bob.png b/screenshots/chat_bob.png
index b12241f..6cf3a7b 100644
Binary files a/screenshots/chat_bob.png and b/screenshots/chat_bob.png differ
diff --git a/screenshots/contact_list.png b/screenshots/contact_list.png
index b3a08a0..3c91bbf 100644
Binary files a/screenshots/contact_list.png and b/screenshots/contact_list.png differ
diff --git a/screenshots/edit_contact.png b/screenshots/edit_contact.png
index e36fdab..a2a9a46 100644
Binary files a/screenshots/edit_contact.png and b/screenshots/edit_contact.png differ