2023-05-03 01:32:46 +00:00
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 ;
2023-05-03 15:17:03 +00:00
2023-05-03 01:32:46 +00:00
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 {
2023-05-03 15:17:03 +00:00
final stream = await Relay . relay . connect ( ) ;
2023-05-03 01:32:46 +00:00
2023-05-03 15:17:03 +00:00
Relay . relay . on ( ( event ) {
2023-05-03 01:32:46 +00:00
if ( event = = RelayEvent . connect ) {
setState ( ( ) = > _isConnected = true ) ;
} else if ( event = = RelayEvent . error ) {
setState ( ( ) = > _isConnected = false ) ;
}
} ) ;
2023-05-03 15:17:03 +00:00
Relay . relay . sub ( [
2023-05-03 01:32:46 +00:00
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 ) ) ;
2023-05-03 15:17:03 +00:00
Relay . relay . sub ( [
2023-05-03 01:32:46 +00:00
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 ,
) ,
2023-05-03 15:17:03 +00:00
Keys . publicKey ,
2023-05-03 01:32:46 +00:00
) ;
if ( eventApi . verifySignature ( event ) ) {
try {
// If the signature is verified, the _relay method is called for the object to publish the event.
2023-05-03 15:17:03 +00:00
Relay . relay . publish ( event ) ;
2023-05-03 01:32:46 +00:00
// 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 ) ,
] ,
)
] ,
) ,
) ,
) ;
} ) ,
) ;
} ,
) ;
}
}