mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-15 11:48:21 +00:00
@ -19,7 +19,7 @@ class BasicButton extends StatelessWidget {
|
|||||||
this.disabled,
|
this.disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
static text(
|
static Widget text(
|
||||||
String text, {
|
String text, {
|
||||||
BoxDecoration? decoration,
|
BoxDecoration? decoration,
|
||||||
EdgeInsetsGeometry? padding,
|
EdgeInsetsGeometry? padding,
|
||||||
|
@ -8,6 +8,7 @@ import 'package:zap_stream_flutter/utils.dart';
|
|||||||
import 'package:zap_stream_flutter/widgets/chat_badge.dart';
|
import 'package:zap_stream_flutter/widgets/chat_badge.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/chat_message.dart';
|
import 'package:zap_stream_flutter/widgets/chat_message.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/chat_raid.dart';
|
import 'package:zap_stream_flutter/widgets/chat_raid.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/chat_timeout.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/chat_write.dart';
|
import 'package:zap_stream_flutter/widgets/chat_write.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/chat_zap.dart';
|
import 'package:zap_stream_flutter/widgets/chat_zap.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/goal.dart';
|
import 'package:zap_stream_flutter/widgets/goal.dart';
|
||||||
@ -20,14 +21,16 @@ class ChatWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var muteLists = [stream.info.host];
|
var muteLists = [stream.info.host];
|
||||||
if (ndk.accounts.getPublicKey() != null) {
|
final myKey = ndk.accounts.getPublicKey();
|
||||||
muteLists.add(ndk.accounts.getPublicKey()!);
|
if (myKey != null) {
|
||||||
|
muteLists.add(myKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
var filters = [
|
var filters = [
|
||||||
Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]),
|
Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]),
|
||||||
Filter(kinds: [1312], limit: 200, aTags: [stream.aTag]),
|
Filter(kinds: [1312, 1313], limit: 200, aTags: [stream.aTag]),
|
||||||
Filter(kinds: [Nip51List.kMute], authors: muteLists),
|
Filter(kinds: [Nip51List.kMute], authors: muteLists),
|
||||||
|
Filter(kinds: [1314], authors: muteLists),
|
||||||
Filter(kinds: [8], authors: [stream.info.host]),
|
Filter(kinds: [8], authors: [stream.info.host]),
|
||||||
];
|
];
|
||||||
return RxFilter<Nip01Event>(
|
return RxFilter<Nip01Event>(
|
||||||
@ -35,26 +38,42 @@ class ChatWidget extends StatelessWidget {
|
|||||||
relays: stream.info.relays,
|
relays: stream.info.relays,
|
||||||
filters: filters,
|
filters: filters,
|
||||||
builder: (ctx, state) {
|
builder: (ctx, state) {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch / 1000;
|
||||||
|
final firstPassEvents = (state ?? []).where(
|
||||||
|
(e) => switch (e.kind) {
|
||||||
|
1314 => muteLists.contains(
|
||||||
|
e.pubKey,
|
||||||
|
), // filter timeouts to only people allowed to mute
|
||||||
|
// TODO: check other kinds are valid for this stream
|
||||||
|
_ => true,
|
||||||
|
},
|
||||||
|
);
|
||||||
final mutedPubkeys =
|
final mutedPubkeys =
|
||||||
(state ?? [])
|
firstPassEvents
|
||||||
.where((e) => e.kind == Nip51List.kMute)
|
.where(
|
||||||
|
(e) =>
|
||||||
|
e.kind == Nip51List.kMute ||
|
||||||
|
(e.kind == 1314 &&
|
||||||
|
e.createdAt < now &&
|
||||||
|
double.parse(e.getFirstTag("expiration")!) > now),
|
||||||
|
)
|
||||||
.map((e) => e.tags)
|
.map((e) => e.tags)
|
||||||
.expand((e) => e)
|
.expand((e) => e)
|
||||||
.where(
|
.where((e) => e[0] == "p")
|
||||||
(e) => e[0] == "p" && e[1] != stream.info.host,
|
|
||||||
) // cant mute host
|
|
||||||
.map((e) => e[1])
|
.map((e) => e[1])
|
||||||
.toSet();
|
.toSet();
|
||||||
|
|
||||||
|
final isChatDisabled = mutedPubkeys.contains(myKey);
|
||||||
final filteredChat =
|
final filteredChat =
|
||||||
(state ?? [])
|
firstPassEvents
|
||||||
.where(
|
.where((e) {
|
||||||
(e) =>
|
final author = switch (e.kind) {
|
||||||
!mutedPubkeys.contains(switch (e.kind) {
|
9735 => ZapReceipt.fromEvent(e).sender ?? e.pubKey,
|
||||||
9735 => ZapReceipt.fromEvent(e).sender ?? e.pubKey,
|
_ => e.pubKey,
|
||||||
_ => e.pubKey,
|
};
|
||||||
}),
|
return muteLists.contains(author) || // cant mute self or host
|
||||||
)
|
!mutedPubkeys.contains(author);
|
||||||
|
})
|
||||||
.sortedBy((e) => e.createdAt)
|
.sortedBy((e) => e.createdAt)
|
||||||
.reversed
|
.reversed
|
||||||
.toList();
|
.toList();
|
||||||
@ -105,6 +124,7 @@ class ChatWidget extends StatelessWidget {
|
|||||||
event: filteredChat[idx],
|
event: filteredChat[idx],
|
||||||
stream: stream,
|
stream: stream,
|
||||||
),
|
),
|
||||||
|
1314 => ChatTimeoutWidget(timeout: filteredChat[idx]),
|
||||||
9735 => ChatZapWidget(
|
9735 => ChatZapWidget(
|
||||||
key: Key("chat:${filteredChat[idx].id}"),
|
key: Key("chat:${filteredChat[idx].id}"),
|
||||||
stream: stream,
|
stream: stream,
|
||||||
@ -118,8 +138,10 @@ class ChatWidget extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (stream.info.status == StreamStatus.live)
|
if (stream.info.status == StreamStatus.live && !isChatDisabled)
|
||||||
WriteMessageWidget(stream: stream),
|
WriteMessageWidget(stream: stream),
|
||||||
|
if (stream.info.status == StreamStatus.live && isChatDisabled)
|
||||||
|
_chatDisabled(filteredChat),
|
||||||
if (stream.info.status == StreamStatus.ended)
|
if (stream.info.status == StreamStatus.ended)
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.all(8),
|
padding: EdgeInsets.all(8),
|
||||||
@ -140,6 +162,37 @@ class ChatWidget extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _chatDisabled(List<Nip01Event> events) {
|
||||||
|
final myKey = ndk.accounts.getPublicKey();
|
||||||
|
final timeoutEvent = events.firstWhereOrNull(
|
||||||
|
(e) => e.kind == 1314 && e.pTags.contains(myKey),
|
||||||
|
);
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
width: double.maxFinite,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(color: WARNING),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text("CHAT DISABLED", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
if (timeoutEvent != null)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text("Timeout expires: "),
|
||||||
|
CountdownTimer(
|
||||||
|
onTrigger: () => {},
|
||||||
|
triggerAt: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
int.parse(timeoutEvent.getFirstTag("expiration")!) * 1000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TopZappersWidget extends StatelessWidget {
|
class _TopZappersWidget extends StatelessWidget {
|
||||||
|
@ -32,7 +32,12 @@ class ChatMessageWidget extends StatelessWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
constraints: BoxConstraints.expand(),
|
constraints: BoxConstraints.expand(),
|
||||||
builder: (ctx) => ChatModalWidget(profile: profile, event: msg),
|
builder:
|
||||||
|
(ctx) => ChatModalWidget(
|
||||||
|
profile: profile,
|
||||||
|
event: msg,
|
||||||
|
stream: stream,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
|
import 'package:duration/duration.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
|
import 'package:zap_stream_flutter/main.dart';
|
||||||
import 'package:zap_stream_flutter/theme.dart';
|
import 'package:zap_stream_flutter/theme.dart';
|
||||||
|
import 'package:zap_stream_flutter/utils.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/button_follow.dart';
|
import 'package:zap_stream_flutter/widgets/button_follow.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/mute_button.dart';
|
import 'package:zap_stream_flutter/widgets/mute_button.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/nostr_text.dart';
|
import 'package:zap_stream_flutter/widgets/nostr_text.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/pill.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/reaction.dart';
|
import 'package:zap_stream_flutter/widgets/reaction.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/zap.dart';
|
import 'package:zap_stream_flutter/widgets/zap.dart';
|
||||||
@ -11,11 +15,13 @@ import 'package:zap_stream_flutter/widgets/zap.dart';
|
|||||||
class ChatModalWidget extends StatefulWidget {
|
class ChatModalWidget extends StatefulWidget {
|
||||||
final Metadata profile;
|
final Metadata profile;
|
||||||
final Nip01Event event;
|
final Nip01Event event;
|
||||||
|
final StreamEvent stream;
|
||||||
|
|
||||||
const ChatModalWidget({
|
const ChatModalWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.profile,
|
required this.profile,
|
||||||
required this.event,
|
required this.event,
|
||||||
|
required this.stream,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -24,9 +30,13 @@ class ChatModalWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _ChatModalWidget extends State<ChatModalWidget> {
|
class _ChatModalWidget extends State<ChatModalWidget> {
|
||||||
bool _showEmojiPicker = false;
|
bool _showEmojiPicker = false;
|
||||||
|
bool _showTimeoutOptions = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isModerator =
|
||||||
|
widget.stream.info.host == ndk.accounts.getPublicKey();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 0),
|
padding: EdgeInsets.fromLTRB(5, 10, 5, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -50,6 +60,7 @@ class _ChatModalWidget extends State<ChatModalWidget> {
|
|||||||
),
|
),
|
||||||
onPressed:
|
onPressed:
|
||||||
() => setState(() {
|
() => setState(() {
|
||||||
|
_showTimeoutOptions = false;
|
||||||
_showEmojiPicker = !_showEmojiPicker;
|
_showEmojiPicker = !_showEmojiPicker;
|
||||||
}),
|
}),
|
||||||
icon: Icon(Icons.mood),
|
icon: Icon(Icons.mood),
|
||||||
@ -76,9 +87,78 @@ class _ChatModalWidget extends State<ChatModalWidget> {
|
|||||||
},
|
},
|
||||||
icon: Icon(Icons.bolt),
|
icon: Icon(Icons.bolt),
|
||||||
),
|
),
|
||||||
|
if (isModerator)
|
||||||
|
IconButton(
|
||||||
|
color: WARNING,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateColor.resolveWith(
|
||||||
|
(_) => LAYER_3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
() => setState(() {
|
||||||
|
_showEmojiPicker = false;
|
||||||
|
_showTimeoutOptions = !_showTimeoutOptions;
|
||||||
|
}),
|
||||||
|
icon: Icon(Icons.timer_outlined),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (_showEmojiPicker) ReactionWidget(event: widget.event),
|
if (_showEmojiPicker) ReactionWidget(event: widget.event),
|
||||||
|
|
||||||
|
if (_showTimeoutOptions)
|
||||||
|
GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
crossAxisCount: 5,
|
||||||
|
childAspectRatio: 3,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
children:
|
||||||
|
[
|
||||||
|
10,
|
||||||
|
30,
|
||||||
|
60,
|
||||||
|
300,
|
||||||
|
60 * 10,
|
||||||
|
60 * 30,
|
||||||
|
60 * 60,
|
||||||
|
60 * 60 * 6,
|
||||||
|
60 * 60 * 12,
|
||||||
|
60 * 60 * 24,
|
||||||
|
60 * 60 * 24 * 2,
|
||||||
|
60 * 60 * 24 * 7,
|
||||||
|
60 * 60 * 24 * 7 * 2,
|
||||||
|
60 * 60 * 24 * 7 * 3,
|
||||||
|
60 * 60 * 24 * 7 * 4,
|
||||||
|
]
|
||||||
|
.map(
|
||||||
|
(v) => PillWidget(
|
||||||
|
color: LAYER_2,
|
||||||
|
onTap: () {
|
||||||
|
final now =
|
||||||
|
(DateTime.now().millisecondsSinceEpoch / 1000)
|
||||||
|
.ceil();
|
||||||
|
final timeout = Nip01Event(
|
||||||
|
pubKey: ndk.accounts.getPublicKey()!,
|
||||||
|
kind: 1314,
|
||||||
|
createdAt: now,
|
||||||
|
tags: [
|
||||||
|
["p", widget.event.pubKey],
|
||||||
|
["expiration", (now + v).toString()],
|
||||||
|
],
|
||||||
|
content: "",
|
||||||
|
);
|
||||||
|
ndk.broadcast.broadcast(nostrEvent: timeout);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
Duration(seconds: v).pretty(abbreviated: true),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
FollowButton(
|
FollowButton(
|
||||||
pubkey: widget.event.pubKey,
|
pubkey: widget.event.pubKey,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:duration/duration.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
@ -202,7 +203,10 @@ class _CountdownTimerState extends State<CountdownTimer>
|
|||||||
animation: _animation,
|
animation: _animation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final secondsLeft = _animation.value.ceil();
|
final secondsLeft = _animation.value.ceil();
|
||||||
return Text(secondsLeft.toString(), style: widget.style);
|
return Text(
|
||||||
|
Duration(seconds: secondsLeft).pretty(abbreviated: true),
|
||||||
|
style: widget.style,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
43
lib/widgets/chat_timeout.dart
Normal file
43
lib/widgets/chat_timeout.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'package:duration/duration.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:ndk/ndk.dart';
|
||||||
|
import 'package:zap_stream_flutter/theme.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||||
|
|
||||||
|
class ChatTimeoutWidget extends StatelessWidget {
|
||||||
|
final Nip01Event timeout;
|
||||||
|
|
||||||
|
const ChatTimeoutWidget({super.key, required this.timeout});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final pTags = timeout.pTags;
|
||||||
|
final duration =
|
||||||
|
double.parse(timeout.getFirstTag("expiration")!) - timeout.createdAt;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: TextStyle(color: LAYER_5),
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: ProfileNameWidget.pubkey(timeout.pubKey),
|
||||||
|
),
|
||||||
|
TextSpan(text: " timed out "),
|
||||||
|
...pTags.map(
|
||||||
|
(p) => WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: ProfileNameWidget.pubkey(p),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: " for ${Duration(seconds: duration.toInt()).pretty()}",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -177,6 +177,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.11"
|
||||||
|
duration:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: duration
|
||||||
|
sha256: "13e5d20723c9c1dde8fb318cf86716d10ce294734e81e44ae1a817f3ae714501"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.3"
|
||||||
elliptic:
|
elliptic:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -34,6 +34,7 @@ dependencies:
|
|||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
flutter_markdown_plus: ^1.0.3
|
flutter_markdown_plus: ^1.0.3
|
||||||
share_plus: ^11.0.0
|
share_plus: ^11.0.0
|
||||||
|
duration: ^4.0.3
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
ndk:
|
ndk:
|
||||||
|
Reference in New Issue
Block a user