Enhanced UserProfilePage to showcase either own or another user's profile
parent
98920a4e61
commit
8120ebda3d
|
@ -130,6 +130,7 @@ class UserTileChats extends StatelessWidget {
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
if (msgDateString.isNotEmpty)
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
msgDateString,
|
msgDateString,
|
||||||
|
|
|
@ -3,17 +3,20 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../components/chat_bubble.dart';
|
import '../components/chat_bubble.dart';
|
||||||
import '../components/my_textfield.dart';
|
import '../components/my_textfield.dart';
|
||||||
|
import '../constants.dart';
|
||||||
import '../services/auth/auth_service.dart';
|
import '../services/auth/auth_service.dart';
|
||||||
import '../services/chat/chat_service.dart';
|
import '../services/chat/chat_service.dart';
|
||||||
|
|
||||||
class ChatPage extends StatefulWidget {
|
class ChatPage extends StatefulWidget {
|
||||||
final String receiverEmail;
|
final String receiverEmail;
|
||||||
final String receiverID;
|
final String receiverID;
|
||||||
|
final String chatTitle;
|
||||||
|
|
||||||
const ChatPage({
|
const ChatPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.receiverEmail,
|
required this.receiverEmail,
|
||||||
required this.receiverID,
|
required this.receiverID,
|
||||||
|
required this.chatTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -88,7 +91,7 @@ class _ChatPageState extends State<ChatPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(widget.receiverEmail)),
|
appBar: AppBar(title: Text(widget.chatTitle)),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// display all messages
|
// display all messages
|
||||||
|
@ -132,12 +135,16 @@ class _ChatPageState extends State<ChatPage> {
|
||||||
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
|
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
|
||||||
|
|
||||||
// align message to the right if sender is current user, otherwise left
|
// align message to the right if sender is current user, otherwise left
|
||||||
bool isCurrentUser = data['senderID'] == _authService.getCurrentUser()!.uid;
|
bool isCurrentUser = data[Constants.dbFieldMessageSenderId] ==
|
||||||
|
_authService.getCurrentUser()!.uid;
|
||||||
var alignment =
|
var alignment =
|
||||||
isCurrentUser ? Alignment.centerRight : Alignment.centerLeft;
|
isCurrentUser ? Alignment.centerRight : Alignment.centerLeft;
|
||||||
|
|
||||||
List<String> msgDate =
|
List<String> msgDate =
|
||||||
(data['timestamp'] as Timestamp).toDate().toIso8601String().split("T");
|
(data[Constants.dbFieldMessageTimestamp] as Timestamp)
|
||||||
|
.toDate()
|
||||||
|
.toIso8601String()
|
||||||
|
.split("T");
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
|
@ -153,7 +160,7 @@ class _ChatPageState extends State<ChatPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ChatBubble(
|
ChatBubble(
|
||||||
message: data['message'],
|
message: data[Constants.dbFieldMessageText],
|
||||||
isCurrentUser: isCurrentUser,
|
isCurrentUser: isCurrentUser,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -78,6 +78,7 @@ class ConversationsPage extends StatelessWidget {
|
||||||
builder: (context) => ChatPage(
|
builder: (context) => ChatPage(
|
||||||
receiverEmail: userData[Constants.dbFieldUsersEmail],
|
receiverEmail: userData[Constants.dbFieldUsersEmail],
|
||||||
receiverID: userData[Constants.dbFieldUsersID],
|
receiverID: userData[Constants.dbFieldUsersID],
|
||||||
|
chatTitle: userData[Constants.dbFieldUsersName],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import '../enumerations.dart';
|
||||||
import '../models/swipe.dart';
|
import '../models/swipe.dart';
|
||||||
import '../services/auth/auth_service.dart';
|
import '../services/auth/auth_service.dart';
|
||||||
import '../utils/helper.dart';
|
import '../utils/helper.dart';
|
||||||
|
import 'user_profile_page.dart';
|
||||||
|
|
||||||
class LikedUsersPage extends StatefulWidget {
|
class LikedUsersPage extends StatefulWidget {
|
||||||
const LikedUsersPage({super.key});
|
const LikedUsersPage({super.key});
|
||||||
|
@ -143,32 +144,6 @@ class LikedUsersPageState extends State<LikedUsersPage> {
|
||||||
return confirm;
|
return confirm;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showUserInfo(DocumentSnapshot user) {
|
|
||||||
Map<String, dynamic> userMap = user.data() as Map<String, dynamic>;
|
|
||||||
bool hasName = userMap.containsKey(Constants.dbFieldUsersName);
|
|
||||||
bool hasBio = userMap.containsKey(Constants.dbFieldUsersBio);
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: hasName
|
|
||||||
? Text(user[Constants.dbFieldUsersName])
|
|
||||||
: const Text('Name: n/a'),
|
|
||||||
content: hasBio
|
|
||||||
? Text(user[Constants.dbFieldUsersBio])
|
|
||||||
: const Text('Bio: n/a'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<MapEntry<Swipe, DocumentSnapshot>> _getSortedLikedUsersWithSwipes() {
|
List<MapEntry<Swipe, DocumentSnapshot>> _getSortedLikedUsersWithSwipes() {
|
||||||
List<MapEntry<Swipe, DocumentSnapshot>> likedOnlyUsers = [];
|
List<MapEntry<Swipe, DocumentSnapshot>> likedOnlyUsers = [];
|
||||||
List<MapEntry<Swipe, DocumentSnapshot>> matchedUsers = [];
|
List<MapEntry<Swipe, DocumentSnapshot>> matchedUsers = [];
|
||||||
|
@ -376,7 +351,16 @@ class LikedUsersPageState extends State<LikedUsersPage> {
|
||||||
}
|
}
|
||||||
}, //_unlikeUser(user.id),
|
}, //_unlikeUser(user.id),
|
||||||
onShowMatchMessage: _showMatchMessage,
|
onShowMatchMessage: _showMatchMessage,
|
||||||
onViewInfo: () => _showUserInfo(user),
|
onViewInfo: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (BuildContext context) => UserProfilePage(
|
||||||
|
userId: user.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -257,6 +257,7 @@ class UserMatchingPageState extends State<UserMatchingPage> {
|
||||||
builder: (context) => ChatPage(
|
builder: (context) => ChatPage(
|
||||||
receiverEmail: swipedUser.email,
|
receiverEmail: swipedUser.email,
|
||||||
receiverID: swipedUser.uid,
|
receiverID: swipedUser.uid,
|
||||||
|
chatTitle: swipedUser.name,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -390,9 +391,6 @@ class UserMatchingPageState extends State<UserMatchingPage> {
|
||||||
Widget _buildUserCard(UserProfile userProfile) {
|
Widget _buildUserCard(UserProfile userProfile) {
|
||||||
String? profileImageUrl = userProfile.profilePictureUrl;
|
String? profileImageUrl = userProfile.profilePictureUrl;
|
||||||
|
|
||||||
int age = calcAge(userProfile.born);
|
|
||||||
String ageInfo = age > 0 ? ' ($age)' : '';
|
|
||||||
|
|
||||||
// Sort the languages according to the given order below
|
// Sort the languages according to the given order below
|
||||||
List<Language> sortedLanguages = List.from(userProfile.languages);
|
List<Language> sortedLanguages = List.from(userProfile.languages);
|
||||||
sortedLanguages.sort((a, b) {
|
sortedLanguages.sort((a, b) {
|
||||||
|
@ -437,7 +435,8 @@ class UserMatchingPageState extends State<UserMatchingPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Center(
|
Center(
|
||||||
child: Text('${userProfile.name}$ageInfo',
|
child: Text(
|
||||||
|
'${userProfile.name} ${ageInfo(userProfile.born)}'.trim(),
|
||||||
style: const TextStyle(fontSize: 24)),
|
style: const TextStyle(fontSize: 24)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
|
@ -15,7 +15,9 @@ import 'user_data_page.dart';
|
||||||
import 'user_vision_page.dart';
|
import 'user_vision_page.dart';
|
||||||
|
|
||||||
class UserProfilePage extends StatefulWidget {
|
class UserProfilePage extends StatefulWidget {
|
||||||
const UserProfilePage({super.key});
|
const UserProfilePage({super.key, this.userId});
|
||||||
|
|
||||||
|
final String? userId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<UserProfilePage> createState() => _UserProfilePageState();
|
State<UserProfilePage> createState() => _UserProfilePageState();
|
||||||
|
@ -25,17 +27,23 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
String? profileImageUrl; // Track the profile image URL
|
String? profileImageUrl; // Track the profile image URL
|
||||||
late UserProfile myData;
|
late UserProfile myData;
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
late bool isOwner;
|
||||||
|
late String _userId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// Determine the userId to use, then check if user is the current user
|
||||||
|
_userId = widget.userId ?? FirebaseAuth.instance.currentUser!.uid;
|
||||||
|
isOwner = (_userId == FirebaseAuth.instance.currentUser!.uid);
|
||||||
|
|
||||||
// Load user data on initialization
|
// Load user data on initialization
|
||||||
_loadUserData();
|
_loadUserData();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadUserData() async {
|
Future<void> _loadUserData() async {
|
||||||
myData = await UserService.getUserProfileById(
|
myData = await UserService.getUserProfileById(_userId);
|
||||||
FirebaseAuth.instance.currentUser!.uid);
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
// Initialize the profile image URL
|
// Initialize the profile image URL
|
||||||
|
@ -193,7 +201,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('My Profile Information'),
|
title: Text(isOwner ? 'My Profile Information' : 'Profile Information'),
|
||||||
),
|
),
|
||||||
body: isLoading
|
body: isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
@ -204,28 +212,34 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
children: [
|
children: [
|
||||||
_buildAvatar(context),
|
_buildAvatar(context),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
if (isOwner)
|
||||||
Divider(color: Theme.of(context).colorScheme.primary),
|
Divider(color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(height: 16),
|
if (isOwner) const SizedBox(height: 16),
|
||||||
_buildLocation(context),
|
_buildLocation(context),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
if (isOwner)
|
||||||
Divider(color: Theme.of(context).colorScheme.primary),
|
Divider(color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(height: 16),
|
if (isOwner) const SizedBox(height: 16),
|
||||||
_buildSkills(context),
|
_buildSkills(context),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
if (isOwner)
|
||||||
Divider(color: Theme.of(context).colorScheme.primary),
|
Divider(color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(height: 16),
|
if (isOwner) const SizedBox(height: 16),
|
||||||
_buildVision(context),
|
_buildVision(context),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
if (isOwner)
|
||||||
Divider(color: Theme.of(context).colorScheme.primary),
|
Divider(color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(height: 16),
|
if (isOwner) const SizedBox(height: 16),
|
||||||
_buildWorkCulture(context),
|
_buildWorkCulture(context),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
if (isOwner)
|
||||||
Divider(color: Theme.of(context).colorScheme.primary),
|
Divider(color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(height: 16),
|
if (isOwner) const SizedBox(height: 16),
|
||||||
_buildRisks(context),
|
_buildRisks(context),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
if (isOwner)
|
||||||
Divider(color: Theme.of(context).colorScheme.primary),
|
Divider(color: Theme.of(context).colorScheme.primary),
|
||||||
const SizedBox(height: 16),
|
if (isOwner) const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -259,6 +273,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isOwner)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Align(
|
child: Align(
|
||||||
|
@ -326,6 +341,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isOwner)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Align(
|
child: Align(
|
||||||
|
@ -396,6 +412,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isOwner)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
|
@ -463,6 +480,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isOwner)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
|
@ -495,6 +513,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isOwner)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
|
@ -521,6 +540,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
if (isOwner)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
@ -550,13 +570,15 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
if (isOwner) const SizedBox(height: 16),
|
||||||
|
if (isOwner)
|
||||||
Text(
|
Text(
|
||||||
'Gender',
|
'Gender',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isOwner)
|
||||||
Text(getDisplayText(myData.gender),
|
Text(getDisplayText(myData.gender),
|
||||||
style: const TextStyle(fontSize: 16)),
|
style: const TextStyle(fontSize: 16)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
@ -597,8 +619,19 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAvatar(BuildContext context) {
|
Widget _buildAvatar(BuildContext context) {
|
||||||
|
Widget genderIcon = const Icon(null);
|
||||||
|
if (myData.gender == Gender.male) {
|
||||||
|
genderIcon = const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 4.0),
|
||||||
|
child: Icon(Icons.male, color: Colors.blue),
|
||||||
|
);
|
||||||
|
} else if (myData.gender == Gender.female) {
|
||||||
|
genderIcon = const Icon(Icons.female, color: Colors.pink);
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (isOwner)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
|
@ -618,8 +651,19 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(myData.name, style: const TextStyle(fontSize: 24)),
|
Row(
|
||||||
Text(myData.email, style: const TextStyle(fontSize: 16)),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isOwner
|
||||||
|
? myData.name
|
||||||
|
: '${myData.name} ${ageInfo(myData.born)}'.trim(),
|
||||||
|
style: const TextStyle(fontSize: 24),
|
||||||
|
),
|
||||||
|
genderIcon,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isOwner) Text(myData.email, style: const TextStyle(fontSize: 16)),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
|
@ -627,7 +671,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Short description of yourself',
|
isOwner ? 'Short description of yourself' : 'Short description',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
|
|
@ -10,6 +10,14 @@ int calcAge(int? birthYear) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the approximate age in parentheses,
|
||||||
|
/// or an empty string if [birthYear] is the current year or null.
|
||||||
|
String ageInfo(int? birthYear) {
|
||||||
|
int age = calcAge(birthYear);
|
||||||
|
String ageInfo = age > 0 ? '($age)' : '';
|
||||||
|
return ageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Convert decimal coordinate to degrees minutes seconds (DMS).
|
/// Convert decimal coordinate to degrees minutes seconds (DMS).
|
||||||
///
|
///
|
||||||
|
@ -45,8 +53,10 @@ double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||||
final dLon = _degreesToRadians(lon2 - lon1);
|
final dLon = _degreesToRadians(lon2 - lon1);
|
||||||
|
|
||||||
final a = sin(dLat / 2) * sin(dLat / 2) +
|
final a = sin(dLat / 2) * sin(dLat / 2) +
|
||||||
cos(_degreesToRadians(lat1)) * cos(_degreesToRadians(lat2)) *
|
cos(_degreesToRadians(lat1)) *
|
||||||
sin(dLon / 2) * sin(dLon / 2);
|
cos(_degreesToRadians(lat2)) *
|
||||||
|
sin(dLon / 2) *
|
||||||
|
sin(dLon / 2);
|
||||||
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||||
|
|
||||||
return R * c;
|
return R * c;
|
||||||
|
|
Loading…
Reference in New Issue