Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/controllers/livekit_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,15 @@ class LiveKitController extends GetxController {
}
}

Future<void> sendData(String message) async {
final data = message.codeUnits;
await liveKitRoom.localParticipant?.publishData(
data,
);
}

void onRoomDidUpdate() {

// Callback which will be called on room update
}

Expand Down
36 changes: 36 additions & 0 deletions lib/controllers/single_room_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class SingleRoomController extends GetxController {
final TablesDB tablesDB = AppwriteService.getTables();
late final RealtimeSubscription? subscription;
RxList<Rx<Participant>> participants = <Rx<Participant>>[].obs;

// For UI visualization only for this PR
RxList<ReactionModel> reactions = <ReactionModel>[].obs;

SingleRoomController({required this.appwriteRoom});

Expand Down Expand Up @@ -371,4 +374,37 @@ class SingleRoomController extends GetxController {
isDismissible: false,
);
}

Future<void> sendReaction(String emoji) async {
final reaction = ReactionModel(
emoji: emoji,
id: DateTime.now().millisecondsSinceEpoch.toString(),
);
reactions.add(reaction);

if (Get.isRegistered<LiveKitController>()) {
await Get.find<LiveKitController>().sendData("REACTION:$emoji");
}
}

void onDataReceived(String message) {
if (message.startsWith("REACTION:")) {
final emoji = message.split(":")[1];
final reaction = ReactionModel(
emoji: emoji,
id: DateTime.now().millisecondsSinceEpoch.toString() + emoji,
);
reactions.add(reaction);
}
}

void removeReaction(String id) {
reactions.removeWhere((r) => r.id == id);
}
}

class ReactionModel {
final String emoji;
final String id;
ReactionModel({required this.emoji, required this.id});
}
71 changes: 59 additions & 12 deletions lib/views/screens/room_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:resonate/controllers/single_room_controller.dart';
import 'package:resonate/models/appwrite_room.dart';
import 'package:resonate/utils/ui_sizes.dart';
import 'package:resonate/views/widgets/participant_block.dart';
import 'package:resonate/views/widgets/reaction_bar.dart';
import 'package:resonate/views/widgets/reaction_floater.dart';
import 'package:resonate/views/widgets/room_app_bar.dart';
import 'package:resonate/views/widgets/room_header.dart';

Expand Down Expand Up @@ -58,20 +60,47 @@ class RoomScreenState extends State<RoomScreen> {
SingleRoomController controller = Get.find<SingleRoomController>();

return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
body: Stack(
children: [
const RoomAppBar(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: RoomHeader(
roomName: widget.room.name,
roomDescription: widget.room.description,
roomTags: _getTags(),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const RoomAppBar(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: RoomHeader(
roomName: widget.room.name,
roomDescription: widget.room.description,
roomTags: _getTags(),
),
),
SizedBox(height: UiSizes.height_7),
Expanded(child: _buildParticipantsList(controller)),
],
),
SizedBox(height: UiSizes.height_7),
Expanded(child: _buildParticipantsList(controller)),

// Reaction Overlay
Obx(() {
return IgnorePointer(
child: Stack(
children: controller.reactions.map((reaction) {
return Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(bottom: 100, right: 20),
child: ReactionFloater(
key: ValueKey(reaction.id),
emoji: reaction.emoji,
onComplete: () {
controller.removeReaction(reaction.id);
},
),
),
);
}).toList(),
),
);
}),
],
),
);
Expand Down Expand Up @@ -177,6 +206,7 @@ class RoomScreenState extends State<RoomScreen> {
children: [
_buildLeaveButton(),
_buildMicButton(),
_buildReactionButton(), // New button
_buildRaiseHandButton(),
_buildChatButton(),
],
Expand All @@ -185,6 +215,23 @@ class RoomScreenState extends State<RoomScreen> {
);
}

Widget _buildReactionButton() {
return FloatingActionButton(
onPressed: () {
Get.bottomSheet(
ReactionBar(
onEmojiSelected: (emoji) {
controller.sendReaction(emoji);
},
),
backgroundColor: Colors.transparent,
);
},
backgroundColor: Colors.lightBlueAccent,
child: const Icon(Icons.mood, color: Colors.white),
);
}

Widget _buildLeaveButton() {
return GetBuilder<SingleRoomController>(
init: SingleRoomController(appwriteRoom: widget.room),
Expand Down
65 changes: 65 additions & 0 deletions lib/views/widgets/reaction_bar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';


class ReactionBar extends StatelessWidget {
final Function(String) onEmojiSelected;

const ReactionBar({super.key, required this.onEmojiSelected});

final List<String> emojis = const ["❤️", "😂", "👏", "🔥", "😮", "🎉"];

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: emojis.map((emoji) {
return GestureDetector(
onTap: () {
onEmojiSelected(emoji);
// Get.back(); // Optional: close sheet or keep open
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3),
shape: BoxShape.circle,
),
child: Text(
emoji,
style: const TextStyle(fontSize: 28),
),
),
);
}).toList(),
),
const SizedBox(height: 20),
],
),
);
}
}
80 changes: 80 additions & 0 deletions lib/views/widgets/reaction_floater.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'dart:math';

class ReactionFloater extends StatefulWidget {
final String emoji;
final VoidCallback onComplete;

const ReactionFloater({
super.key,
required this.emoji,
required this.onComplete,
});

@override
State<ReactionFloater> createState() => _ReactionFloaterState();
}

class _ReactionFloaterState extends State<ReactionFloater>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacityAnimation;
late Animation<double> _scaleAnimation;
late Animation<Offset> _positionAnimation;
late double _randomX;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);

_randomX = (Random().nextDouble() * 100) - 50; // Random X offset (-50 to 50)

_opacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
),
);

_scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.elasticOut),
),
);

_positionAnimation = Tween<Offset>(
begin: const Offset(0, 0),
end: Offset(_randomX * 0.01, -2.5), // Float up significantly
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));

_controller.forward().then((_) => widget.onComplete());
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return SlideTransition(
position: _positionAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: FadeTransition(
opacity: _opacityAnimation,
child: Text(
widget.emoji,
style: const TextStyle(fontSize: 32),
),
),
),
);
}
}