Skip to content
2 changes: 2 additions & 0 deletions commet/lib/client/components/profile/profile_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ abstract class UserProfileComponent<T extends Client> implements Component<T> {

Future<void> removeBio();

Future<void> removeBanner();

Future<void> removeTimezone();

Future<List<ProfileBadge>> getAvailableBadges();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ abstract class SpaceBannerComponent<R extends Client, T extends Space>
bool get canEditBanner;

Future<void> setBanner(Uint8List data, {String? mimeType});
Future<void> removeBanner();
}
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ class MatrixProfileComponent implements UserProfileComponent<MatrixClient> {
await setField(bannerKey, mxc.toString());
}

@override
Future<void> removeBanner() async {
await removeField(bannerKey);
}

Future<void> setField(String field, dynamic content) async {
final data = {field: content};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class MatrixSpaceBannerComponent
final url = state.content["url"] as String?;
if (url == null) return null;

if (!url.startsWith("mxc://")) return null;

return MatrixMxcImage(Uri.parse(url), client.matrixClient);
}

Expand Down Expand Up @@ -47,6 +49,17 @@ class MatrixSpaceBannerComponent
space.notifyUpdate();
}

@override
Future<void> removeBanner() async {
await client.matrixClient.setRoomStateWithKey(
space.matrixRoom.id,
key,
'',
{},
);
space.notifyUpdate();
}

@override
bool get canEditBanner => space.matrixRoom.canChangeStateEvent(key);
}
200 changes: 200 additions & 0 deletions commet/lib/ui/molecules/image_select_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import 'package:commet/config/layout_config.dart';
import 'package:commet/ui/navigation/adaptive_dialog.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:commet/utils/common_strings.dart';
import 'package:tiamat/tiamat.dart' as tiamat;

enum ImageEditAction {
cancel,
remove,
pick,
}

class ImageSelectDialog extends StatelessWidget {
static Future<ImageEditAction?> show(
BuildContext context, {
required ImageProvider? image,
String? title,
}) {
return AdaptiveDialog.show<ImageEditAction>(
context,
title: title ?? changeImageDialogTitle,
scrollable: false,
builder: (dialogContext) {
return ImageSelectDialog(
image: image,
onCancel: () =>
Navigator.of(dialogContext).pop(ImageEditAction.cancel),
onRemove: () =>
Navigator.of(dialogContext).pop(ImageEditAction.remove),
onPick: () => Navigator.of(dialogContext).pop(ImageEditAction.pick),
);
},
);
}

const ImageSelectDialog({
super.key,
required this.image,
required this.onCancel,
required this.onRemove,
required this.onPick,
});

static String get changeImageDialogTitle => Intl.message("Change Image",
name: "changeImageDialogTitle",
desc: "Title for the dialog used to change an image");

String get removeImagePrompt => Intl.message("Remove Image",
name: "removeImagePrompt", desc: "Button text for removing an image");

String get pickImagePrompt => Intl.message("Pick Image",
name: "pickImagePrompt", desc: "Button text for picking an image");

String get confirmRemovePrompt =>
Intl.message("Are you sure you want to remove this image?",
name: "confirmRemovePrompt",
desc: "Prompt text for confirming image removal");

String get emptyImagePrompt => Intl.message("No image set",
name: "emptyImagePrompt",
desc: "Text shown when there is no image to display");

final ImageProvider? image;
final VoidCallback onCancel;
final VoidCallback onRemove;
final VoidCallback onPick;

@override
Widget build(BuildContext context) {
Widget buttons = Row(
spacing: 8,
children: [
Expanded(
child: tiamat.Button.secondary(
text: CommonStrings.promptCancel,
onTap: onCancel,
),
),
Expanded(
child: tiamat.Button(
text: removeImagePrompt,
type: tiamat.ButtonType.danger,
onTap: () async {
if (await confirmRemove(context)) {
onRemove();
}
},
),
),
Expanded(
child: tiamat.Button(
text: pickImagePrompt,
onTap: onPick,
),
),
],
);

if (Layout.mobile) {
buttons = Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
tiamat.Button(
text: pickImagePrompt,
onTap: onPick,
),
tiamat.Button(
text: removeImagePrompt,
type: tiamat.ButtonType.danger,
onTap: () async {
if (await confirmRemove(context)) {
onRemove();
}
},
),
tiamat.Button.secondary(
text: CommonStrings.promptCancel,
onTap: onCancel,
),
],
);
}

return SizedBox(
width: 700,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 12,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio: 700 / 230,
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
image: image != null
? DecorationImage(
image: image!,
fit: BoxFit.cover,
)
: null,
),
child: image == null
? Center(
child: tiamat.Text.labelLow(emptyImagePrompt),
)
: null,
),
),
),
buttons,
],
),
);
}

Future<bool> confirmRemove(BuildContext context) async {
final confirmed = await AdaptiveDialog.show<bool>(
context,
title: removeImagePrompt,
scrollable: false,
builder: (dialogContext) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 12,
children: [
tiamat.Text.body(
confirmRemovePrompt,
),
Row(
spacing: 8,
children: [
Expanded(
child: tiamat.Button.secondary(
text: CommonStrings.promptCancel,
onTap: () => Navigator.of(dialogContext).pop(false),
),
),
Expanded(
child: tiamat.Button(
text: CommonStrings.promptRemove,
type: tiamat.ButtonType.danger,
onTap: () => Navigator.of(dialogContext).pop(true),
),
),
],
),
],
);
},
);

return confirmed ?? false;
}
}
23 changes: 23 additions & 0 deletions commet/lib/ui/organisms/user_profile/user_profile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:commet/client/matrix/matrix_mxc_image_provider.dart';
import 'package:commet/config/layout_config.dart';
import 'package:commet/debug/log.dart';
import 'package:commet/ui/atoms/code_block.dart';
import 'package:commet/ui/molecules/image_select_dialog.dart';
import 'package:commet/ui/molecules/message_input.dart';
import 'package:commet/ui/navigation/adaptive_dialog.dart';
import 'package:commet/utils/event_bus.dart';
Expand Down Expand Up @@ -270,6 +271,20 @@ class _UserProfileState extends State<UserProfile> {
}

Future<void> setBanner() async {
final action = await ImageSelectDialog.show(
context,
image: banner,
);

if (!mounted) return;

if (action == ImageEditAction.remove) {
await removeBanner();
return;
}

if (action != ImageEditAction.pick) return;

var result =
await PickerUtils.pickImageAndCrop(context, aspectRatio: 700 / 230);
if (result == null) return;
Expand All @@ -281,6 +296,14 @@ class _UserProfileState extends State<UserProfile> {
await component.setBanner(result);
}

Future<void> removeBanner() async {
await component.removeBanner();

setState(() {
banner = null;
});
}

Color previewColor = Colors.blue;
Brightness previewBrightness = Brightness.dark;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:typed_data';

import 'package:commet/client/client.dart';
import 'package:commet/client/components/space_banner/space_banner_component.dart';
import 'package:commet/ui/molecules/image_select_dialog.dart';
import 'package:commet/ui/pages/settings/categories/room/appearance/room_appearance_settings_view.dart';
import 'package:commet/utils/picker_utils.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -76,6 +77,27 @@ class _SpaceAppearanceSettingsPageState
color: Colors.transparent,
child: InkWell(
onTap: () async {
final action = await ImageSelectDialog.show(
context,
image: image,
);

if (action == ImageEditAction.remove) {
setState(() {
image = null;
uploading = true;
});
await widget.space
.getComponent<SpaceBannerComponent>()
?.removeBanner();
setState(() {
uploading = false;
});
return;
} else if (action != ImageEditAction.pick) {
return;
}

var result = await PickerUtils.pickImageAndCrop(context,
aspectRatio: 16 / 9);

Expand Down
8 changes: 8 additions & 0 deletions commet/lib/utils/common_strings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ class CommonStrings {
static String get promptCopy =>
Intl.message("Copy", desc: "Prompt to copy text", name: "promptCopy");

static String get promptCancel => Intl.message("Cancel",
desc: "Generic prompt to cancel an action, usually shown on a button",
name: "promptCancel");

static String get promptRemove => Intl.message("Remove",
desc: "Generic prompt to remove something, usually shown on a button",
name: "promptRemove");

static String get labelPublic =>
Intl.message("Public", desc: "Label for public", name: "labelPublic");

Expand Down
7 changes: 6 additions & 1 deletion commet/lib/utils/picker_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:crop_image/crop_image.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:tiamat/tiamat.dart' as tiamat;

class PickerUtils {
Expand Down Expand Up @@ -76,13 +77,17 @@ class ImageCropView extends StatelessWidget {

final double width = 1000;

String get useWithoutCroppingPrompt => Intl.message("Use Without Cropping",
name: "useWithoutCroppingPrompt",
desc: "Button text for using an image without cropping it");

@override
Widget build(BuildContext context) {
var buttons = [
Expanded(
flex: Layout.desktop ? 1 : 0,
child: tiamat.Button.secondary(
text: "Use Original Image",
text: useWithoutCroppingPrompt,
onTap: () async {
onImageSubmitted?.call(imageBytes);
},
Expand Down
Loading