-
-
Notifications
You must be signed in to change notification settings - Fork 68
Add Google Meet integration to meeting creation #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:intl/intl.dart'; | ||
| import '../../services/supabase_service.dart'; | ||
| import '../../services/google_meet_service.dart'; | ||
|
|
||
| class CreateMeetingScreen extends StatefulWidget { | ||
| const CreateMeetingScreen({super.key}); | ||
|
|
@@ -15,11 +16,14 @@ class _CreateMeetingScreenState extends State<CreateMeetingScreen> { | |
| final _descriptionController = TextEditingController(); | ||
| final _urlController = TextEditingController(); | ||
| final _durationController = TextEditingController(text: '60'); // Default to 60 minutes | ||
|
|
||
| final _supabaseService = SupabaseService(); | ||
| final _googleMeetService = GoogleMeetService(); | ||
|
|
||
| DateTime? _selectedDate; | ||
| TimeOfDay? _selectedTime; | ||
| bool _isLoading = false; | ||
| bool _isCreatingMeetLink = false; | ||
| bool _isGoogleMeetUrl = true; | ||
|
|
||
| @override | ||
|
|
@@ -44,6 +48,85 @@ class _CreateMeetingScreenState extends State<CreateMeetingScreen> { | |
| }); | ||
| } | ||
|
|
||
| Future<void> _createGoogleMeetLink() async { | ||
| if (_selectedDate == null || _selectedTime == null) { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('Please select date and time first'), | ||
| backgroundColor: Colors.red, | ||
| ), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Combine date and time | ||
| final meetingDateTime = DateTime( | ||
| _selectedDate!.year, | ||
| _selectedDate!.month, | ||
| _selectedDate!.day, | ||
| _selectedTime!.hour, | ||
| _selectedTime!.minute, | ||
| ); | ||
|
|
||
| // Validate meeting is in the future | ||
| if (meetingDateTime.isBefore(DateTime.now())) { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('Meeting time must be in the future'), | ||
| backgroundColor: Colors.red, | ||
| ), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Parse duration | ||
| int duration = 60; | ||
| try { | ||
| duration = int.parse(_durationController.text.trim()); | ||
| if (duration <= 0) duration = 60; | ||
| } catch (e) { | ||
| // Default to 60 if parsing fails | ||
| duration = 60; | ||
| } | ||
|
|
||
| setState(() => _isCreatingMeetLink = true); | ||
|
|
||
| final link = await _googleMeetService.createMeetLink( | ||
| start: meetingDateTime, | ||
| durationMinutes: duration, | ||
| title: _titleController.text.trim().isNotEmpty | ||
| ? _titleController.text.trim() | ||
| : 'Meeting', | ||
| description: _descriptionController.text.trim(), | ||
| ); | ||
|
|
||
| if (!mounted) return; | ||
|
|
||
| setState(() => _isCreatingMeetLink = false); | ||
|
|
||
| if (link != null) { | ||
| _urlController.text = link; | ||
| _checkUrl(link); | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('Google Meet link created successfully'), | ||
| backgroundColor: Colors.green, | ||
| ), | ||
| ); | ||
| } else { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar( | ||
| content: const Text('Failed to create Google Meet link. Please sign in with Google.'), | ||
| backgroundColor: Colors.orange, | ||
| action: SnackBarAction( | ||
| label: 'Retry', | ||
| onPressed: () => _createGoogleMeetLink(), | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Future<void> _createMeeting() async { | ||
| if (!_formKey.currentState!.validate()) return; | ||
|
|
||
|
|
@@ -384,6 +467,32 @@ class _CreateMeetingScreenState extends State<CreateMeetingScreen> { | |
| }, | ||
| ), | ||
|
|
||
| // Create Google Meet Link button | ||
| const SizedBox(height: 8), | ||
| SizedBox( | ||
| width: double.infinity, | ||
| child: ElevatedButton.icon( | ||
| onPressed: _isCreatingMeetLink ? null : _createGoogleMeetLink, | ||
| icon: _isCreatingMeetLink | ||
| ? const SizedBox( | ||
| width: 16, | ||
| height: 16, | ||
| child: CircularProgressIndicator( | ||
| strokeWidth: 2, | ||
| color: Colors.white, | ||
| ), | ||
| ) | ||
| : const Icon(Icons.video_call), | ||
| label: Text( | ||
| _isCreatingMeetLink ? 'Creating Meet Link...' : 'Create Google Meet Link', | ||
| ), | ||
| style: ElevatedButton.styleFrom( | ||
| backgroundColor: Colors.blue.shade800, | ||
| padding: const EdgeInsets.symmetric(vertical: 16), | ||
| ), | ||
| ), | ||
| ), | ||
|
Comment on lines
+470
to
+494
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Button UI and loading state look correct. The button is properly disabled during link creation, shows a One minor UX note: the "Create Meeting" button (Line 512) is not disabled while Suggested fix for the Create Meeting button SizedBox(
width: double.infinity,
child: ElevatedButton(
- onPressed: _createMeeting,
+ onPressed: _isCreatingMeetLink ? null : _createMeeting,π€ Prompt for AI Agents |
||
|
|
||
| // Warning message for non-Google Meet URLs | ||
| if (_urlController.text.isNotEmpty && !_isGoogleMeetUrl) | ||
| Padding( | ||
|
|
@@ -417,4 +526,4 @@ class _CreateMeetingScreenState extends State<CreateMeetingScreen> { | |
| ), | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: #!/bin/bash
# Check for test files and GoogleMeetService references
echo "=== Searching for test files with Google Meet related patterns ==="
find . -path './test*' -name '*.dart' -type f | head -50
echo -e "\n=== Searching for GoogleMeetService references in test files ==="
rg -i 'googlemeet|google_meet|createmeetlink' --type=dart -g '*test*' --max-count=20
echo -e "\n=== Checking if GoogleMeetService exists in lib ==="
rg -l 'class GoogleMeetService|GoogleMeetService' --type=dart -g '!test'
echo -e "\n=== Looking for test directory structure ==="
find . -type d -name test | head -20Repository: AOSSIE-Org/Ell-ena Length of output: 410 π Script executed: #!/bin/bash
# Search for any meet-related test files
echo "=== Looking for meet-related test files ==="
fd -e dart . test -i meet
echo -e "\n=== Checking all test files ==="
find . -path '*/test/*' -name '*test.dart' -o -name '*_test.dart' | wc -l
echo -e "\n=== Searching for GoogleMeetService in all dart files ==="
rg 'GoogleMeetService' --type=dartRepository: AOSSIE-Org/Ell-ena Length of output: 451 π Script executed: #!/bin/bash
echo "=== Contents of the only test file ==="
cat ./test/widget_test.dartRepository: AOSSIE-Org/Ell-ena Length of output: 1187 Add unit and widget tests for Google Meet functionality. The codebase currently has only a basic counter smoke test. Tests for π€ Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import 'package:google_sign_in/google_sign_in.dart'; | ||
| import 'package:googleapis/calendar/v3.dart' as cal; | ||
| import 'package:googleapis_auth/auth_io.dart'; | ||
| import 'package:http/http.dart' as http; | ||
|
|
||
| class GoogleMeetService { | ||
| final GoogleSignIn _googleSignIn = GoogleSignIn( | ||
| scopes: [ | ||
| cal.CalendarApi.calendarScope, | ||
| cal.CalendarApi.calendarEventsScope, | ||
| ], | ||
| ); | ||
|
|
||
| Future<String?> createMeetLink({ | ||
| required DateTime start, | ||
| required int durationMinutes, | ||
| required String title, | ||
| String? description, | ||
| }) async { | ||
| try { | ||
| // Sign in with Google | ||
| final account = await _googleSignIn.signIn(); | ||
| if (account == null) return null; | ||
|
|
||
| // Get authentication | ||
| final authentication = await account.authentication; | ||
| final accessToken = authentication.accessToken; | ||
|
|
||
| if (accessToken == null) { | ||
| print('No access token available'); | ||
| return null; | ||
| } | ||
|
|
||
| // Create authenticated client using authenticatedClient function (FIXED) | ||
| final credentials = AccessCredentials( | ||
| AccessToken('Bearer', accessToken, DateTime.now().add(const Duration(hours: 1)).toUtc()), | ||
|
Comment on lines
+35
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded token expiry is unreliable. The actual access token lifespan is determined by Google's auth server and may differ from the assumed 1-hour window. π€ Prompt for AI Agents |
||
| null, // No refresh token needed for one-time use | ||
| _googleSignIn.scopes, | ||
| ); | ||
|
|
||
| final authClient = authenticatedClient(http.Client(), credentials); | ||
|
|
||
| try { | ||
| final calendarApi = cal.CalendarApi(authClient); | ||
|
|
||
| final event = cal.Event( | ||
| summary: title, | ||
| description: description, | ||
| start: cal.EventDateTime(dateTime: start.toUtc()), | ||
| end: cal.EventDateTime( | ||
| dateTime: start.add(Duration(minutes: durationMinutes)).toUtc(), | ||
| ), | ||
| conferenceData: cal.ConferenceData( | ||
| createRequest: cal.CreateConferenceRequest( | ||
| requestId: DateTime.now().millisecondsSinceEpoch.toString(), | ||
| conferenceSolutionKey: cal.ConferenceSolutionKey( | ||
| type: 'hangoutsMeet', | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| final createdEvent = await calendarApi.events.insert( | ||
| event, | ||
| 'primary', | ||
| conferenceDataVersion: 1, | ||
| ); | ||
|
|
||
| // Return the meet link | ||
| return createdEvent.hangoutLink ?? | ||
| createdEvent.conferenceData?.entryPoints | ||
| ?.firstWhere( | ||
| (e) => e.entryPointType == 'video', | ||
| orElse: () => cal.EntryPoint(uri: ''), | ||
| ) | ||
| .uri; | ||
|
Comment on lines
+69
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fallback can return an empty string instead of When Suggested fix- return createdEvent.hangoutLink ??
- createdEvent.conferenceData?.entryPoints
- ?.firstWhere(
- (e) => e.entryPointType == 'video',
- orElse: () => cal.EntryPoint(uri: ''),
- )
- .uri;
+ final link = createdEvent.hangoutLink ??
+ createdEvent.conferenceData?.entryPoints
+ ?.firstWhere(
+ (e) => e.entryPointType == 'video',
+ orElse: () => cal.EntryPoint(),
+ )
+ .uri;
+ return (link != null && link.isNotEmpty) ? link : null;π€ Prompt for AI Agents |
||
| } finally { | ||
| authClient.close(); | ||
| } | ||
| } catch (e) { | ||
| print('Error creating Google Meet link: $e'); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| Future<void> signOut() async { | ||
| await _googleSignIn.signOut(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against empty-string Meet link from the service.
As noted in the service review,
createMeetLinkcan return""instead ofnull. Even after fixing the service, a defensive check here prevents a broken link from silently populating the URL field.Suggested fix
π Committable suggestion
π€ Prompt for AI Agents