Displaying keys as npub and nsec

This commit is contained in:
Alex Vasilev 2023-05-03 18:17:03 +03:00
parent bcd1735920
commit 5b5cf93674
22 changed files with 1579 additions and 10 deletions

View File

@ -0,0 +1,4 @@
import 'package:flutter/material.dart';
String avatarUrl = "https://sweary.com/avatar-generator/";
String imageSize = "size=200x200";

View File

@ -0,0 +1,21 @@
class Domain {
final String noteId;
final String avatarUrl;
final String name;
final String username;
final String time;
final String content;
final String pubkey;
final String? imageUrl;
Domain({
required this.noteId,
required this.avatarUrl,
required this.name,
required this.username,
required this.time,
required this.content,
required this.pubkey,
this.imageUrl,
});
}

29
lib/lib/main.dart Normal file
View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
import 'package:drifter/widgets/main_screen/main_screen_widget.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
appBarTheme: const AppBarTheme(backgroundColor: AppColors.mainDarkBlue),
primarySwatch: Colors.blue,
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: AppColors.mainDarkBlue,
selectedItemColor: Colors.white,
unselectedItemColor: Colors.grey,
),
),
home: const MainScreenWidget(),
);
}
}

12
lib/lib/models/keys.dart Normal file
View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
import 'package:nostr_tools/nostr_tools.dart';
class Keys {
static String privateKey = '';
static String publicKey = '';
static bool keysExist = false;
}
class Relay {
static final relay = RelayApi(relayUrl: 'wss://relay.damus.io');
}

View File

@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
abstract class AppColors {
static const mainDarkBlue = Color.fromRGBO(3, 37, 65, 1);
static const mainLightBlue = Color.fromRGBO(48, 86, 117, 1);
}

View File

@ -0,0 +1,442 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:drifter/domain_models/domain_models.dart';
import 'package:drifter/models/keys.dart';
import 'package:drifter/theme/app_colors.dart';
import 'package:drifter/widgets/home_screen/home_screen_widgets/message_ok_button_widget.dart';
import 'package:drifter/widgets/home_screen/home_screen_widgets/message_text_button_widget.dart';
import 'package:drifter/widgets/home_screen/home_screen_widgets/message_text_form_field_widget.dart';
import 'package:drifter/widgets/profile_screen/profile_screen_widgets/message_snack_bar.dart';
import 'package:nostr_tools/nostr_tools.dart';
import '../profile_screen/profile_screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _isConnected = false;
final List<Event> _events = [];
final Map<String, Metadata> _metaDatas = {};
late Stream<Event> _stream;
final _controller = StreamController<Event>();
bool _isNotePublishing = false;
@override
void initState() {
_initStream();
super.initState();
}
Future<Stream<Event>> _connectToRelay() async {
final stream = await Relay.relay.connect();
Relay.relay.on((event) {
if (event == RelayEvent.connect) {
setState(() => _isConnected = true);
} else if (event == RelayEvent.error) {
setState(() => _isConnected = false);
}
});
Relay.relay.sub([
Filter(
kinds: [1],
limit: 100,
t: ['nostr'],
)
]);
return stream
.where((message) => message.type == 'EVENT')
.map((message) => message.message);
}
void _initStream() async {
_stream = await _connectToRelay();
_stream.listen((message) {
final event = message;
if (event.kind == 1) {
setState(() => _events.add(event));
Relay.relay.sub([
Filter(kinds: [0], authors: [event.pubkey])
]);
} else if (event.kind == 0) {
final metadata = Metadata.fromJson(jsonDecode(event.content));
setState(() => _metaDatas[event.pubkey] = metadata);
}
_controller.add(event);
});
}
// The _resubscribeStream() method clears the _events and _metaData scollection after a 1 second delay
Future<void> _resubscribeStream() async {
await Future.delayed(const Duration(seconds: 1), () {
setState(() {
_events.clear();
_metaDatas.clear();
});
// _initStream() method, responsible for initializing and subscribing to the stream, to reconnect and re-subscribe to the filter.
_initStream();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: () async {
await _resubscribeStream();
},
child: StreamBuilder(
stream: _controller.stream,
builder: (context, snapshot) {
// Inside the builder callback, the snapshot object contains the most recent event from the thread.
// If snapshot.hasData is true, there is data to display. In this case, ListView.builder is returned, which displays a list of NoostCard widgets.
if (snapshot.hasData) {
return ListView.builder(
// The itemCount property of ListView.builder is set to _events.length, , which is the number of events in the _events list.
itemCount: _events.length,
itemBuilder: (context, index) {
final event = _events[index];
final metadata = _metaDatas[event.pubkey];
// For each event, a Noost object is created that encapsulates the details of the event, including id, avatarUrl, name,username, time, content и pubkey.
// _metaDatas, you can map the event public key to the author's metadata.
final domain = Domain(
noteId: event.id,
avatarUrl: metadata?.picture ??
'https://robohash.org/${event.pubkey}',
name: metadata?.name ?? 'Anon',
username: metadata?.displayName ??
(metadata?.display_name ?? 'Anon'),
time: TimeAgo.format(event.created_at),
content: event.content,
pubkey: event.pubkey,
);
return DomainCard(domain: domain);
},
);
} else if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: Text('Loading....'));
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
return const CenteredCircularProgressIndicator();
},
),
),
floatingActionButton: Keys.keysExist
? CreatePost(
// The publishNote function is called when the user launches the "Noost!" button in the dialog box.
publishNote: (note) {
setState(() => _isNotePublishing = true);
// EventApi Creates an instance of the class defined in the nostr_tools package.
final eventApi = EventApi();
// The finishEvent method of the EventApi class is called with the Event object and the _privateKey variable.
// finishEvent will set the event id with the event hash and sign the event with the given _privateKey.
final event = eventApi.finishEvent(
// This creates a new instance of the Event class with certain properties, such as:
Event(
kind: 1,
tags: [
['t', 'nostr']
],
content: note!,
created_at: DateTime.now().millisecondsSinceEpoch ~/ 1000,
),
Keys.publicKey,
);
if (eventApi.verifySignature(event)) {
try {
// If the signature is verified, the _relay method is called for the object to publish the event.
Relay.relay.publish(event);
// After the _resubscribeStream event is published, a method is called that will probably update the stream or subscription to reflect the recently published event.
_resubscribeStream();
// Show SnackBar to display a message that the note has been successfully published.
ScaffoldMessenger.of(context).showSnackBar(
MessageSnackBar(
label: 'Congratulations! Noost Published!'),
);
} catch (_) {
// If an error occurs during the publishing process (e.g., an exception is caught), SnackBar displays a warning instead.
ScaffoldMessenger.of(context).showSnackBar(MessageSnackBar(
label: 'Oops! Something went wrong!',
isWarning: true,
));
}
}
setState(() => _isNotePublishing = false);
Navigator.pop(context);
},
isNotePublishing: _isNotePublishing,
)
:
// If _keysExist is false, then an empty widget is displayed, which means that the FAB will not be visible. Container()
Container(),
);
}
}
class TimeAgo {
static String format(int timestamp) {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
Duration difference = DateTime.now().difference(dateTime);
String timeAgo = '';
if (difference.inDays > 0) {
timeAgo =
'${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago';
} else if (difference.inHours > 0) {
timeAgo =
'${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago';
} else if (difference.inMinutes > 0) {
timeAgo =
'${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago';
} else {
timeAgo = 'just now';
}
return timeAgo;
}
}
class DomainCard extends StatelessWidget {
const DomainCard({
super.key,
required this.domain,
});
final Domain domain;
List<String>? extractImage(String text) {
final RegExp exp = RegExp(
r"(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png|jpeg)",
caseSensitive: false,
multiLine: true,
);
final Iterable<Match> matches = exp.allMatches(text);
final List<String> imageLinks =
matches.map((match) => match.group(0)!).toList();
return imageLinks.isNotEmpty ? imageLinks : null;
}
@override
Widget build(BuildContext context) {
final List<String>? imageLinks = extractImage(domain.content);
return Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.mainLightBlue,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: CircleAvatar(
backgroundImage: FadeInImage(
placeholder:
const NetworkImage('https://i.ibb.co/mJkxDkb/satoshi.png'),
image: NetworkImage(domain.avatarUrl),
).image,
),
title:
Text(domain.name, style: const TextStyle(color: Colors.white)),
subtitle: Text('@${domain.username.toLowerCase()}${domain.time}',
style: TextStyle(color: Colors.grey.shade400)),
trailing: const Icon(Icons.more_vert, color: Colors.grey),
),
Divider(height: 1, color: Colors.grey.shade400),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(domain.content,
style: const TextStyle(color: Colors.white)),
),
if (imageLinks != null && imageLinks.isNotEmpty)
Center(
child: Stack(
children: [
const Placeholder(
fallbackHeight: 200,
color: Colors.transparent,
),
Center(
child: FadeInImage(
placeholder: const NetworkImage(
'https://i.ibb.co/D9jqXgR/58038897-167f0280-7ae6-11e9-94eb-88e880a25f0f.gif',
),
image: NetworkImage(imageLinks.first),
fit: BoxFit.cover,
),
),
],
),
),
],
),
);
}
}
class CenteredCircularProgressIndicator extends StatelessWidget {
const CenteredCircularProgressIndicator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}
class CreatePost extends StatefulWidget {
const CreatePost({
Key? key,
required this.publishNote,
required this.isNotePublishing,
}) : super(key: key);
final Function(String?) publishNote;
final bool isNotePublishing;
@override
State<CreatePost> createState() => _CreatePostState();
}
class _CreatePostState extends State<CreatePost> {
final _noteController = TextEditingController();
final GlobalKey<FormFieldState> _formKey = GlobalKey<FormFieldState>();
@override
Widget build(BuildContext context) {
return FloatingActionButton(
backgroundColor: Colors.deepPurpleAccent,
tooltip: 'Create a new post',
elevation: 2,
highlightElevation: 4,
foregroundColor: Colors.white,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 60,
height: 60,
decoration: const BoxDecoration(
shape: BoxShape.circle, color: AppColors.mainDarkBlue),
),
const Icon(
Icons.add,
color: Colors.white,
),
],
),
onPressed: () async {
_noteController.clear();
await showDialog(
barrierDismissible: false,
context: context,
builder: ((context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppColors.mainDarkBlue,
),
child: const Center(
child: Text(
'Create a Noost',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: MessageTextFormField(
maxLines: 5,
hintText: 'Type your Noost here...',
controller: _noteController,
formKey: _formKey,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter your note.';
}
return null;
},
),
),
const SizedBox(height: 24),
widget.isNotePublishing
? const CenteredCircularProgressIndicator()
: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MessageTextButton(
onPressed: () {
Navigator.pop(context);
},
label: 'Cancel',
),
const SizedBox(width: 16),
MessageOkButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
widget.publishNote(
_noteController.text.trim());
} else {
_formKey.currentState?.setState(() {});
}
},
label: 'Noost!',
),
const SizedBox(width: 24),
],
)
],
),
),
);
}),
);
},
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
class MessageOkButton extends StatelessWidget {
const MessageOkButton({
super.key,
required this.onPressed,
required this.label,
});
final void Function()? onPressed;
final String label;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.mainDarkBlue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
class MessageTextButton extends StatelessWidget {
const MessageTextButton({
super.key,
required this.onPressed,
required this.label,
this.color,
});
final void Function()? onPressed;
final String label;
final Color? color;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
child: Text(
label,
style: TextStyle(
color: AppColors.mainDarkBlue,
),
),
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
class MessageTextFormField extends StatelessWidget {
const MessageTextFormField({
super.key,
required this.hintText,
required this.controller,
required this.formKey,
required this.validator,
this.maxLines,
});
final String hintText;
final TextEditingController controller;
final Key formKey;
final String? Function(String?)? validator;
final int? maxLines;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
key: formKey,
validator: validator,
maxLines: maxLines,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: hintText,
hintStyle: const TextStyle(color: Colors.black54),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.black54),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.black54),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.mainDarkBlue),
),
),
);
}
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:drifter/widgets/home_screen/home_screen_widget.dart';
import 'package:drifter/widgets/message_screen/message_screen_widget.dart';
import 'package:drifter/widgets/profile_screen/profile_screen.dart';
class MainScreenWidget extends StatefulWidget {
const MainScreenWidget({super.key});
@override
State<MainScreenWidget> createState() => _MainScreenWidgetState();
}
class _MainScreenWidgetState extends State<MainScreenWidget> {
int _selectedTap = 0;
void onSelectedtap(int index) {
if (_selectedTap == index) return;
setState(() {
_selectedTap = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Drifter'),
centerTitle: true,
),
body: IndexedStack(
index: _selectedTap,
children: [
HomeScreen(),
// MessageScreen(),
ProfileScreen(),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedTap,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
// BottomNavigationBarItem(
// icon: Icon(Icons.message),
// label: 'Message',
// ),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
],
onTap: onSelectedtap,
),
);
}
}

View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
class MessageScreen extends StatefulWidget {
const MessageScreen({super.key});
@override
State<MessageScreen> createState() => _MessageScreenState();
}
class _MessageScreenState extends State<MessageScreen> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
ListView(
padding: EdgeInsets.all(16),
children: [
Center(
child: Text('List of posts'),
)
],
),
MessageInput(),
],
);
}
}
class MessageInput extends StatefulWidget {
const MessageInput({
super.key,
});
@override
State<MessageInput> createState() => _MessageInputState();
}
class _MessageInputState extends State<MessageInput> {
final messageController = TextEditingController();
void submitData() {
final newMessage = messageController;
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.only(bottom: 16, left: 8, right: 8, top: 8),
child: Row(
children: [
GestureDetector(
onTap: () {},
child: const Icon(
Icons.add_a_photo,
color: AppColors.mainDarkBlue,
size: 30,
),
),
const SizedBox(
width: 8,
),
Expanded(
child: TextField(
style: const TextStyle(fontSize: 20),
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20))),
hintText: 'What\'s new?',
hintStyle: const TextStyle(fontSize: 20),
suffixIcon: IconButton(
icon: const Icon(
Icons.send,
color: AppColors.mainDarkBlue,
size: 30,
),
onPressed: () {
submitData();
},
),
),
),
),
],
),
),
);
}
}
class NewMessage extends StatefulWidget {
const NewMessage({super.key});
@override
State<NewMessage> createState() => _NewMessageState();
}
class _NewMessageState extends State<NewMessage> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -0,0 +1,271 @@
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:drifter/models/keys.dart';
import 'package:drifter/theme/app_colors.dart';
import 'package:nostr_tools/nostr_tools.dart';
import 'profile_screen_widgets/delete_keys_dialog.dart';
import 'profile_screen_widgets/key_exist_dialog.dart';
import 'profile_screen_widgets/keys_option_modal_bottom_sheet.dart';
import 'profile_screen_widgets/message_snack_bar.dart';
import 'profile_screen_widgets/user_info_widget.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => ProfileScreenState();
}
class ProfileScreenState extends State<ProfileScreen> {
final _secureStorage = const FlutterSecureStorage();
TextEditingController privateKeyInput = TextEditingController();
TextEditingController publicKeyInput = TextEditingController();
TextEditingController relayInput = TextEditingController();
final keyGenerator = KeyApi();
final nip19 = Nip19();
void initState() {
_getKeysFromStorage();
super.initState();
}
Future<bool> generateNewKeys() async {
final newPrivateKey = keyGenerator.generatePrivateKey();
final nsec = nip19.nsecEncode(newPrivateKey);
final nsecDecoded = nip19.decode(nsec);
assert(nsecDecoded['type'] == 'nsec');
assert(nsecDecoded['data'] == newPrivateKey);
final newPublicKey = keyGenerator.getPublicKey(newPrivateKey);
final npub = nip19.npubEncode(newPublicKey);
final npubDecoded = nip19.decode(npub);
assert(npubDecoded['type'] == 'npub');
assert(npubDecoded['data'] == newPublicKey);
return await _addKeyToStorage(nsec, npub);
}
Future<void> _getKeysFromStorage() async {
// Reading values associated with the " privateKey " and " publicKey " keys from a secure repository
final storedPrivateKey = await _secureStorage.read(key: 'privateKey');
final storedPublicKey = await _secureStorage.read(key: 'publicKey');
// Indicates that both private and public keys are stored in a secure repository, after which, the state variables are updated
if (storedPrivateKey != null && storedPublicKey != null) {
setState(() {
Keys.privateKey = storedPrivateKey;
Keys.publicKey = storedPublicKey;
Keys.keysExist = true;
});
}
}
// Adding a new key
// Writing a private and public key to a secure vault
Future<bool> _addKeyToStorage(
String privateKeyHex,
String publicKeyHex,
) async {
// Waiting for both write operations to complete
Future.wait([
_secureStorage.write(key: 'privateKey', value: privateKeyHex),
_secureStorage.write(key: 'publicKey', value: privateKeyHex),
]);
// Updating status variables and starting widget rebuilding
setState(() {
Keys.privateKey = privateKeyHex;
Keys.publicKey = publicKeyHex;
Keys.keysExist = true;
});
// Returns a boolean value indicating whether the keys were successfully added to the repository or not.
return Keys.keysExist;
}
Future<void> _deleteKeysStorage() async {
// Calling secure storage to remove keys from storage
Future.wait([
_secureStorage.delete(key: 'privateKey'),
_secureStorage.delete(key: 'publicKey'),
]);
// Updating status variables, resetting values after deleting keys from the repository
setState(() {
Keys.privateKey = '';
Keys.publicKey = '';
Keys.keysExist = false;
});
}
@override
Widget build(BuildContext context) {
privateKeyInput.text = Keys.privateKey;
publicKeyInput.text = Keys.publicKey;
relayInput.text = Relay.relay.relayUrl;
return ListView(
children: [
SizedBox(
height: 60,
),
UserInfo(),
SizedBox(
height: 40,
),
FormKeys(),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Keys.keysExist
? ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
AppColors.mainDarkBlue)),
onPressed: () {
keysExistDialog(
nip19.npubEncode(Keys.publicKey),
nip19.nsecEncode(Keys.privateKey),
);
},
child: Text(
'Keys',
),
)
: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
AppColors.mainDarkBlue)),
onPressed: () {
modalBottomSheet();
},
child: Text(
'Generate Keys',
),
),
Keys.keysExist
? Row(
children: [
IconButton(
onPressed: () {
deleteKeysDialog();
},
icon: const Icon(Icons.delete)),
],
)
: Container(),
],
),
)
],
);
}
Form FormKeys() {
return Form(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
controller: privateKeyInput,
// _toHex ? widget.hexPriv : widget.nsecEncoded,
decoration: const InputDecoration(
labelText: 'Private Key',
border: OutlineInputBorder(),
),
),
const SizedBox(
height: 20,
),
TextFormField(
controller: publicKeyInput,
// _toHex ? widget.hexPub : widget.npubEncoded,
decoration: const InputDecoration(
labelText: 'Public Key',
border: OutlineInputBorder(),
),
),
const SizedBox(
height: 40,
),
TextFormField(
controller: relayInput,
decoration: const InputDecoration(
labelText: 'Relay',
border: OutlineInputBorder(),
),
),
],
),
),
);
}
void modalBottomSheet() {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return KeysOptionModalBottomSheet(
generateNewKeyPressed: () {
final currentContext = context;
generateNewKeys().then(
(keysGenerated) {
if (keysGenerated) {
ScaffoldMessenger.of(currentContext).showSnackBar(
MessageSnackBar(label: 'Keys Generated!'));
}
},
);
Navigator.pop(context);
},
);
});
}
void keysExistDialog(String npubEncode, String nsecEncode) async {
await showDialog(
context: context,
builder: ((context) {
return KeysExistDialog(
npubEncoded: npubEncode,
nsecEncoded: nsecEncode,
hexPriv: Keys.privateKey,
hexPub: Keys.publicKey,
);
}),
);
}
void deleteKeysDialog() async {
await showDialog(
context: context,
builder: ((context) {
return DeleteKeysDialog(
onNoPressed: () {
Navigator.pop(context);
},
onYesPressed: () {
final currentContext = context;
_deleteKeysStorage().then((_) {
if (!Keys.keysExist) {
ScaffoldMessenger.of(currentContext).showSnackBar(
MessageSnackBar(
label: 'Keys successfully deleted!',
isWarning: true,
),
);
}
});
Navigator.pop(context);
},
);
}),
);
}
}

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
import 'ok_button_widget.dart';
class DeleteKeysDialog extends StatelessWidget {
const DeleteKeysDialog({
super.key,
required this.onNoPressed,
required this.onYesPressed,
});
final void Function()? onNoPressed;
final void Function()? onYesPressed;
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.redAccent,
),
child: const Center(
child: Text(
'Delete Keys!',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Do you want to delete your keys?',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: onNoPressed,
child: Text(
'On',
style: TextStyle(color: AppColors.mainDarkBlue),
),
),
OkButton(
onPressed: onYesPressed,
label: 'YES',
),
],
),
],
),
)
],
),
),
);
}
}

View File

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'ok_button_widget.dart';
class GeneratedKeys extends StatefulWidget {
const GeneratedKeys({
super.key,
required this.npubEncoded,
required this.nsecEncoded,
required this.hexPriv,
required this.hexPub,
});
final String npubEncoded;
final String nsecEncoded;
final String hexPriv;
final String hexPub;
@override
State<GeneratedKeys> createState() => _GeneratedKeysState();
}
class _GeneratedKeysState extends State<GeneratedKeys> {
bool _toHex = false;
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.indigo,
),
child: const Center(
child: Text(
'Keys',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Public Key',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 12),
SelectableText(
_toHex ? widget.hexPub : widget.npubEncoded,
style: TextStyle(
fontSize: 16,
color: Colors.grey[800],
),
),
const SizedBox(height: 24),
Text(
'Private Key',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 12),
SelectableText(
_toHex ? widget.hexPriv : widget.nsecEncoded,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.redAccent,
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment
.spaceBetween, // Changed to space between to create space for icon buttons
children: [
IconButton(
onPressed: () {
setState(() {
_toHex = !_toHex;
});
},
icon: const Icon(Icons.autorenew_outlined),
color: Colors.grey[700],
),
OkButton(
onPressed: () {
Navigator.pop(context);
},
label: 'OK',
),
],
)
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
import 'ok_button_widget.dart';
class KeysExistDialog extends StatefulWidget {
const KeysExistDialog({
super.key,
required this.npubEncoded,
required this.nsecEncoded,
required this.hexPriv,
required this.hexPub,
});
final String npubEncoded;
final String nsecEncoded;
final String hexPriv;
final String hexPub;
@override
State<KeysExistDialog> createState() => _KeysExistDialogState();
}
class _KeysExistDialogState extends State<KeysExistDialog> {
bool _toHex = false;
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppColors.mainDarkBlue,
),
child: const Center(
child: Text(
'Keys',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Public Key',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 12),
SelectableText(
_toHex ? widget.hexPub : widget.npubEncoded,
style: TextStyle(
fontSize: 16,
color: Colors.grey[800],
),
),
const SizedBox(height: 24),
Text(
'Private Key',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 12),
SelectableText(
_toHex ? widget.hexPriv : widget.nsecEncoded,
style: TextStyle(
fontSize: 16,
color: Colors.grey[800],
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment
.spaceBetween, // Changed to space between to create space for icon buttons
children: [
IconButton(
onPressed: () {
setState(() {
_toHex = !_toHex;
});
},
icon: const Icon(Icons.autorenew_outlined),
color: Colors.grey[700],
),
OkButton(
onPressed: () {
Navigator.pop(context);
},
label: 'OK',
),
],
)
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
class KeysOptionModalBottomSheet extends StatelessWidget {
const KeysOptionModalBottomSheet({
super.key,
required this.generateNewKeyPressed,
});
final void Function()? generateNewKeyPressed;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: AppColors.mainDarkBlue),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.white)),
onPressed: generateNewKeyPressed,
child: Text(
'Generate New Key',
style: TextStyle(color: AppColors.mainDarkBlue),
),
),
const SizedBox(height: 10),
],
),
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
class MessageSnackBar extends SnackBar {
MessageSnackBar({Key? key, required this.label, this.isWarning = false})
: super(
key: key,
content: _GenericErrorSnackBarMessage(
label: label,
isWarning: isWarning,
),
backgroundColor: isWarning! ? Colors.red : Colors.white,
elevation: 6.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
behavior: SnackBarBehavior.fixed,
);
final String label;
final bool? isWarning;
}
class _GenericErrorSnackBarMessage extends StatelessWidget {
const _GenericErrorSnackBarMessage({
Key? key,
required this.label,
this.isWarning,
}) : super(key: key);
final String label;
final bool? isWarning;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Text(
label,
style: TextStyle(
color: isWarning! ? Colors.white : Colors.black,
fontSize: 16.0,
),
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:drifter/theme/app_colors.dart';
class OkButton extends StatelessWidget {
const OkButton({
super.key,
required this.onPressed,
required this.label,
});
final void Function()? onPressed;
final String label;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.mainDarkBlue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
class UserInfo extends StatelessWidget {
const UserInfo({super.key});
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
AvatarWidget(),
SizedBox(
height: 10,
),
UserNameWidget(),
],
),
);
}
}
class AvatarWidget extends StatelessWidget {
const AvatarWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.all(Radius.circular(75))),
width: 150,
height: 150,
);
}
}
class UserNameWidget extends StatelessWidget {
const UserNameWidget({super.key});
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: Text(
'Username',
style: TextStyle(fontSize: 25),
),
);
}
}

View File

@ -1,7 +1,12 @@
import 'package:flutter/material.dart';
import 'package:nostr_tools/nostr_tools.dart';
class Keys {
static String privateKey = '';
static String publicKey = '';
static bool keysExist = false;
}
class Relay {
static final relay = RelayApi(relayUrl: 'wss://relay.damus.io');
}

View File

@ -22,7 +22,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> {
bool _isConnected = false;
final _relay = RelayApi(relayUrl: 'wss://relay.damus.io');
final List<Event> _events = [];
final Map<String, Metadata> _metaDatas = {};
late Stream<Event> _stream;
@ -37,9 +37,9 @@ class _HomeScreenState extends State<HomeScreen> {
}
Future<Stream<Event>> _connectToRelay() async {
final stream = await _relay.connect();
final stream = await Relay.relay.connect();
_relay.on((event) {
Relay.relay.on((event) {
if (event == RelayEvent.connect) {
setState(() => _isConnected = true);
} else if (event == RelayEvent.error) {
@ -47,7 +47,7 @@ class _HomeScreenState extends State<HomeScreen> {
}
});
_relay.sub([
Relay.relay.sub([
Filter(
kinds: [1],
limit: 100,
@ -66,7 +66,7 @@ class _HomeScreenState extends State<HomeScreen> {
final event = message;
if (event.kind == 1) {
setState(() => _events.add(event));
_relay.sub([
Relay.relay.sub([
Filter(kinds: [0], authors: [event.pubkey])
]);
} else if (event.kind == 0) {
@ -153,12 +153,12 @@ class _HomeScreenState extends State<HomeScreen> {
content: note!,
created_at: DateTime.now().millisecondsSinceEpoch ~/ 1000,
),
Keys.privateKey,
Keys.publicKey,
);
if (eventApi.verifySignature(event)) {
try {
// If the signature is verified, the _relay method is called for the object to publish the event.
_relay.publish(event);
Relay.relay.publish(event);
// After the _resubscribeStream event is published, a method is called that will probably update the stream or subscription to reflect the recently published event.
_resubscribeStream();
// Show SnackBar to display a message that the note has been successfully published.

View File

@ -8,7 +8,6 @@ import 'profile_screen_widgets/delete_keys_dialog.dart';
import 'profile_screen_widgets/key_exist_dialog.dart';
import 'profile_screen_widgets/keys_option_modal_bottom_sheet.dart';
import 'profile_screen_widgets/message_snack_bar.dart';
import 'profile_screen_widgets/ok_button_widget.dart';
import 'profile_screen_widgets/user_info_widget.dart';
class ProfileScreen extends StatefulWidget {
@ -23,6 +22,7 @@ class ProfileScreenState extends State<ProfileScreen> {
TextEditingController privateKeyInput = TextEditingController();
TextEditingController publicKeyInput = TextEditingController();
TextEditingController relayInput = TextEditingController();
final keyGenerator = KeyApi();
final nip19 = Nip19();
@ -44,7 +44,7 @@ class ProfileScreenState extends State<ProfileScreen> {
final npubDecoded = nip19.decode(npub);
assert(npubDecoded['type'] == 'npub');
assert(npubDecoded['data'] == newPublicKey);
return await _addKeyToStorage(newPrivateKey, newPublicKey);
return await _addKeyToStorage(nsec, npub);
}
Future<void> _getKeysFromStorage() async {
@ -103,6 +103,7 @@ class ProfileScreenState extends State<ProfileScreen> {
Widget build(BuildContext context) {
privateKeyInput.text = Keys.privateKey;
publicKeyInput.text = Keys.publicKey;
relayInput.text = Relay.relay.relayUrl;
return ListView(
children: [
@ -178,7 +179,6 @@ class ProfileScreenState extends State<ProfileScreen> {
labelText: 'Private Key',
border: OutlineInputBorder(),
),
maxLength: 64,
),
const SizedBox(
height: 20,
@ -195,6 +195,7 @@ class ProfileScreenState extends State<ProfileScreen> {
height: 40,
),
TextFormField(
controller: relayInput,
decoration: const InputDecoration(
labelText: 'Relay',
border: OutlineInputBorder(),