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,
|
||||
});
|
||||
|
||||
static text(
|
||||
static Widget text(
|
||||
String text, {
|
||||
BoxDecoration? decoration,
|
||||
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_message.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_zap.dart';
|
||||
import 'package:zap_stream_flutter/widgets/goal.dart';
|
||||
@ -20,14 +21,16 @@ class ChatWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var muteLists = [stream.info.host];
|
||||
if (ndk.accounts.getPublicKey() != null) {
|
||||
muteLists.add(ndk.accounts.getPublicKey()!);
|
||||
final myKey = ndk.accounts.getPublicKey();
|
||||
if (myKey != null) {
|
||||
muteLists.add(myKey);
|
||||
}
|
||||
|
||||
var filters = [
|
||||
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: [1314], authors: muteLists),
|
||||
Filter(kinds: [8], authors: [stream.info.host]),
|
||||
];
|
||||
return RxFilter<Nip01Event>(
|
||||
@ -35,26 +38,42 @@ class ChatWidget extends StatelessWidget {
|
||||
relays: stream.info.relays,
|
||||
filters: filters,
|
||||
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 =
|
||||
(state ?? [])
|
||||
.where((e) => e.kind == Nip51List.kMute)
|
||||
firstPassEvents
|
||||
.where(
|
||||
(e) =>
|
||||
e.kind == Nip51List.kMute ||
|
||||
(e.kind == 1314 &&
|
||||
e.createdAt < now &&
|
||||
double.parse(e.getFirstTag("expiration")!) > now),
|
||||
)
|
||||
.map((e) => e.tags)
|
||||
.expand((e) => e)
|
||||
.where(
|
||||
(e) => e[0] == "p" && e[1] != stream.info.host,
|
||||
) // cant mute host
|
||||
.where((e) => e[0] == "p")
|
||||
.map((e) => e[1])
|
||||
.toSet();
|
||||
|
||||
final isChatDisabled = mutedPubkeys.contains(myKey);
|
||||
final filteredChat =
|
||||
(state ?? [])
|
||||
.where(
|
||||
(e) =>
|
||||
!mutedPubkeys.contains(switch (e.kind) {
|
||||
9735 => ZapReceipt.fromEvent(e).sender ?? e.pubKey,
|
||||
_ => e.pubKey,
|
||||
}),
|
||||
)
|
||||
firstPassEvents
|
||||
.where((e) {
|
||||
final author = switch (e.kind) {
|
||||
9735 => ZapReceipt.fromEvent(e).sender ?? e.pubKey,
|
||||
_ => e.pubKey,
|
||||
};
|
||||
return muteLists.contains(author) || // cant mute self or host
|
||||
!mutedPubkeys.contains(author);
|
||||
})
|
||||
.sortedBy((e) => e.createdAt)
|
||||
.reversed
|
||||
.toList();
|
||||
@ -105,6 +124,7 @@ class ChatWidget extends StatelessWidget {
|
||||
event: filteredChat[idx],
|
||||
stream: stream,
|
||||
),
|
||||
1314 => ChatTimeoutWidget(timeout: filteredChat[idx]),
|
||||
9735 => ChatZapWidget(
|
||||
key: Key("chat:${filteredChat[idx].id}"),
|
||||
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),
|
||||
if (stream.info.status == StreamStatus.live && isChatDisabled)
|
||||
_chatDisabled(filteredChat),
|
||||
if (stream.info.status == StreamStatus.ended)
|
||||
Container(
|
||||
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 {
|
||||
|
@ -32,7 +32,12 @@ class ChatMessageWidget extends StatelessWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
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:ndk/ndk.dart';
|
||||
import 'package:zap_stream_flutter/main.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/mute_button.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/reaction.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 {
|
||||
final Metadata profile;
|
||||
final Nip01Event event;
|
||||
final StreamEvent stream;
|
||||
|
||||
const ChatModalWidget({
|
||||
super.key,
|
||||
required this.profile,
|
||||
required this.event,
|
||||
required this.stream,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -24,9 +30,13 @@ class ChatModalWidget extends StatefulWidget {
|
||||
|
||||
class _ChatModalWidget extends State<ChatModalWidget> {
|
||||
bool _showEmojiPicker = false;
|
||||
bool _showTimeoutOptions = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isModerator =
|
||||
widget.stream.info.host == ndk.accounts.getPublicKey();
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 0),
|
||||
child: Column(
|
||||
@ -50,6 +60,7 @@ class _ChatModalWidget extends State<ChatModalWidget> {
|
||||
),
|
||||
onPressed:
|
||||
() => setState(() {
|
||||
_showTimeoutOptions = false;
|
||||
_showEmojiPicker = !_showEmojiPicker;
|
||||
}),
|
||||
icon: Icon(Icons.mood),
|
||||
@ -76,9 +87,78 @@ class _ChatModalWidget extends State<ChatModalWidget> {
|
||||
},
|
||||
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 (_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(
|
||||
pubkey: widget.event.pubKey,
|
||||
onTap: () {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:duration/duration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
@ -202,7 +203,10 @@ class _CountdownTimerState extends State<CountdownTimer>
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
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"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
duration:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: duration
|
||||
sha256: "13e5d20723c9c1dde8fb318cf86716d10ce294734e81e44ae1a817f3ae714501"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
elliptic:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -34,6 +34,7 @@ dependencies:
|
||||
intl: ^0.20.2
|
||||
flutter_markdown_plus: ^1.0.3
|
||||
share_plus: ^11.0.0
|
||||
duration: ^4.0.3
|
||||
|
||||
dependency_overrides:
|
||||
ndk:
|
||||
|
Reference in New Issue
Block a user