Skip to content
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

LB-1688 Feature to thank a user for a pin/recommendation #3143

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
41f2f04
Completed backend and partial frontend
Suvid-Singhal Jan 23, 2025
7cf9400
Completed backend and partial frontend
Suvid-Singhal Jan 23, 2025
9786ba1
Added frontend
Suvid-Singhal Jan 23, 2025
92457d3
Merge branch 'master' into thanks
Suvid-Singhal Jan 23, 2025
31cecaa
Lint python code
Suvid-Singhal Jan 23, 2025
d791c11
Lint python code
Suvid-Singhal Jan 23, 2025
d4fce99
Lint code
Suvid-Singhal Jan 23, 2025
67d1f6e
Lint
Suvid-Singhal Jan 23, 2025
819800b
lint
Suvid-Singhal Jan 23, 2025
472a031
Fix backend error caused due to linter
Suvid-Singhal Jan 24, 2025
7585ef1
Fixed frontend and backend
Suvid-Singhal Jan 26, 2025
be69c96
Merge branch 'master' into thanks
Suvid-Singhal Jan 26, 2025
68b304b
Updated db to add thanks type and tried to fix backend code
Suvid-Singhal Jan 27, 2025
b9a0f29
Add value to the correct enum
Suvid-Singhal Jan 27, 2025
84ba8b5
Merge branch 'master' into thanks
Suvid-Singhal Jan 27, 2025
a84dd52
Updated code, still WIP
Suvid-Singhal Jan 28, 2025
6ef247b
Merge branch 'master' into thanks
Suvid-Singhal Jan 28, 2025
bb27439
Merge branch 'master' into thanks
Suvid-Singhal Jan 28, 2025
c3df99b
Backend kinda works now
Suvid-Singhal Jan 29, 2025
9d83507
Merge branch 'master' into thanks
Suvid-Singhal Jan 29, 2025
b0e6e71
Merge branch 'master' into thanks
Suvid-Singhal Jan 31, 2025
7c4a75f
Backend finally functioning as intended
Suvid-Singhal Feb 1, 2025
9799af9
Merge branch 'master' into thanks
Suvid-Singhal Feb 1, 2025
dd5f88c
Merge branch 'master' into thanks
Suvid-Singhal Feb 3, 2025
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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
"editor.defaultFormatter": "ms-python.autopep8"
},
"python.formatting.provider": "none"
}
2 changes: 1 addition & 1 deletion admin/sql/create_types.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ CREATE TYPE user_relationship_enum AS ENUM('follow');

CREATE TYPE recommendation_feedback_type_enum AS ENUM('like', 'love', 'dislike', 'hate', 'bad_recommendation');

CREATE TYPE user_timeline_event_type_enum AS ENUM('recording_recommendation', 'notification', 'critiquebrainz_review', 'personal_recording_recommendation');
CREATE TYPE user_timeline_event_type_enum AS ENUM('recording_recommendation', 'notification', 'critiquebrainz_review', 'personal_recording_recommendation', 'thanks');

CREATE TYPE hide_user_timeline_event_type_enum AS ENUM('recording_recommendation', 'personal_recording_recommendation', 'recording_pin');

Expand Down
1 change: 1 addition & 0 deletions admin/sql/updates/2025-01-27-add-thanks-event-type.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE user_timeline_event_type_enum ADD VALUE 'thanks' AFTER 'personal_recording_recommendation';
161 changes: 161 additions & 0 deletions frontend/js/src/user-feed/ThanksModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import * as React from "react";
import NiceModal, { useModal } from "@ebay/nice-modal-react";
import { toast } from "react-toastify";
import GlobalAppContext from "../utils/GlobalAppContext";
import { ToastMsg } from "../notifications/Notifications";

export type ThanksModalProps = {
original_event_id?: number;
original_event_type: EventTypeT;
};

export const maxBlurbContentLength = 280;

/** A note about this modal:
* We use Bootstrap 3 modals, which work with jQuery and data- attributes
* In order to show the modal properly, including backdrop and visibility,
* you'll need dataToggle="modal" and dataTarget="#ThanksModal"
* on the buttons that open this modal as well as data-dismiss="modal"
* on the buttons that close the modal. Modals won't work (be visible) without it
* until we move to Bootstrap 5 / Bootstrap React which don't require those attributes.
*/

export default NiceModal.create(
({ original_event_id, original_event_type }: ThanksModalProps) => {
// Use a hook to manage the modal state
const modal = useModal();
const [blurbContent, setBlurbContent] = React.useState("");

const { APIService, currentUser } = React.useContext(GlobalAppContext);

const handleError = React.useCallback(
(error: string | Error, title?: string): void => {
if (!error) {
return;
}
toast.error(
<ToastMsg
title={title || "Error"}
message={typeof error === "object" ? error.message : error}
/>,
{ toastId: "thank-error" }
);
},
[]
);

const handleBlurbInputChange = React.useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
event.preventDefault();
const input = event.target.value.replace(/\s\s+/g, " "); // remove line breaks and excessive spaces
if (input.length <= maxBlurbContentLength) {
setBlurbContent(input);
}
},
[]
);

const closeModal = () => {
modal.hide();
document?.body?.classList?.remove("modal-open");
setTimeout(modal.remove, 200);
};

const submitThanks = React.useCallback(
async (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
if (currentUser?.auth_token) {
try {
const status = await APIService.thankFeedEvent(
original_event_id,
original_event_type,
currentUser.auth_token,
currentUser.name,
blurbContent
);
if (status === 200) {
toast.success(
<ToastMsg
title={`You thanked this ${original_event_type}`}
message="OK"
/>,
{ toastId: "thanks-success" }
);
closeModal();
}
} catch (error) {
handleError(error, "Error while thanking");
}
}
},
[blurbContent]
);

return (
<div
className={`modal fade ${modal.visible ? "in" : ""}`}
id="ThanksModal"
tabIndex={-1}
role="dialog"
aria-labelledby="ThanksModalLabel"
data-backdrop="static"
>
<div className="modal-dialog" role="document">
<form className="modal-content">
<div className="modal-header">
<button
type="button"
className="close"
data-dismiss="modal"
aria-label="Close"
onClick={closeModal}
>
<span aria-hidden="true">&times;</span>
</button>
<h4 className="modal-title" id="ThanksModalLabel">
Thank <b>{original_event_type}</b>
</h4>
</div>
<div className="modal-body">
<p>Leave a message (optional)</p>
<div className="form-group">
<textarea
className="form-control"
id="blurb-content"
placeholder="A thank you message..."
value={blurbContent}
name="blurb-content"
rows={4}
style={{ resize: "vertical" }}
onChange={handleBlurbInputChange}
/>
</div>
<small className="character-count">
{blurbContent.length} / {maxBlurbContentLength}
<br />
</small>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-default"
data-dismiss="modal"
onClick={closeModal}
>
Cancel
</button>
<button
type="submit"
className="btn btn-success"
data-dismiss="modal"
onClick={submitThanks}
>
Send Thanks
</button>
</div>
</form>
</div>
</div>
);
}
);
68 changes: 57 additions & 11 deletions frontend/js/src/user-feed/UserFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
faComments,
faEye,
faEyeSlash,
faHandshake,
faHeadphones,
faHeart,
faPaperPlane,
Expand Down Expand Up @@ -50,6 +51,7 @@ import {
personalRecommendationEventToListen,
preciseTimestamp,
} from "../utils/utils";
import ThanksModal from "./ThanksModal";

export enum EventType {
RECORDING_RECOMMENDATION = "recording_recommendation",
Expand All @@ -62,6 +64,7 @@ export enum EventType {
BLOCK_FOLLOW = "block_follow",
NOTIFICATION = "notification",
REVIEW = "critiquebrainz_review",
THANKS = "thanks",
}

export type UserFeedPageProps = {
Expand All @@ -86,7 +89,8 @@ function isEventListenable(event?: TimelineEvent): boolean {
event_type === EventType.LIKE ||
event_type === EventType.LISTEN ||
event_type === EventType.REVIEW ||
event_type === EventType.PERSONAL_RECORDING_RECOMMENDATION
event_type === EventType.PERSONAL_RECORDING_RECOMMENDATION ||
event_type === EventType.THANKS
);
}

Expand All @@ -112,6 +116,8 @@ function getEventTypeIcon(eventType: EventTypeT) {
return faComments;
case EventType.PERSONAL_RECORDING_RECOMMENDATION:
return faPaperPlane;
case EventType.THANKS:
return faHandshake;
default:
return faQuestion;
}
Expand Down Expand Up @@ -148,6 +154,8 @@ function getEventTypePhrase(event: TimelineEvent): string {
}
case EventType.PERSONAL_RECORDING_RECOMMENDATION:
return "personally recommended a track";
case EventType.THANKS:
return `thanked a ${event.event_type}`;
default:
return "";
}
Expand Down Expand Up @@ -257,6 +265,7 @@ export default function UserFeedPage() {
},
[APIService, currentUser]
);

// When this mutation succeeds, modify the query cache accordingly to avoid refetching all the content
const { mutate: hideEventMutation } = useMutation({
mutationFn: changeEventVisibility,
Expand Down Expand Up @@ -424,16 +433,34 @@ export default function UserFeedPage() {
);
}
return (
<ListenControl
title="Hide Event"
text=""
icon={faEyeSlash}
buttonClassName="btn btn-link btn-xs"
// eslint-disable-next-line react/jsx-no-bind
action={() => {
hideEventMutation(event);
}}
/>
<>
<ListenControl
title="Thanks"
text=""
icon={faHandshake}
buttonClassName="btn btn-link btn-xs"
action={() => {
console.log(event.id);
console.log(event.event_type);
NiceModal.show(ThanksModal, {
original_event_id: event.id!,
original_event_type: event.event_type,
});
}}
dataToggle="modal"
dataTarget="#ThanksModal"
/>
<ListenControl
title="Hide Event"
text=""
icon={faEyeSlash}
buttonClassName="btn btn-link btn-xs"
// eslint-disable-next-line react/jsx-no-bind
action={() => {
hideEventMutation(event);
}}
/>
</>
);
}
return null;
Expand Down Expand Up @@ -544,6 +571,25 @@ export default function UserFeedPage() {
/>
);
}
if (event_type === EventType.THANKS) {
console.log(metadata);
const {
original_event_id,
original_event_type,
thanker_id,
thanker_username,
thankee_id,
thankee_username,
blurb_content,
} = metadata as ThanksMetadata;

return (
<>
<Username username={thanker_username} /> thanked{" "}
<Username username={thankee_username} />
</>
);
}

const userLinkOrYou =
user_name === currentUser.name ? (
Expand Down
29 changes: 29 additions & 0 deletions frontend/js/src/utils/APIService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,35 @@ export default class APIService {
return response.status;
};

thankFeedEvent = async (
event_id: number | undefined,
eventType: EventTypeT,
userToken: string,
username: string,
blurb_content: string
): Promise<any> => {
if (!event_id) {
throw new SyntaxError("Event ID not present");
}
const query = `${this.APIBaseURI}/user/${username}/timeline-event/create/thanks`;
const response = await fetch(query, {
method: "POST",
headers: {
Authorization: `Token ${userToken}`,
"Content-Type": "application/json;charset=UTF-8",
},
body: JSON.stringify({
metadata: {
original_event_type: eventType,
original_event_id: event_id,
blurb_content,
},
}),
});
await this.checkStatus(response);
return response.status;
};

lookupRecordingMetadata = async (
trackName: string,
artistName: string,
Expand Down
16 changes: 14 additions & 2 deletions frontend/js/src/utils/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ type EventTypeT =
| "block_follow"
| "notification"
| "personal_recording_recommendation"
| "critiquebrainz_review";
| "critiquebrainz_review"
| "thanks";

type UserRelationshipEventMetadata = {
user_name_0: string;
Expand All @@ -486,6 +487,16 @@ type UserRelationshipEventMetadata = {
created: number;
};

type ThanksMetadata = {
original_event_id: number;
original_event_type: EventTypeT;
blurb_content: string;
thanker_id: number;
thanker_username: string;
thankee_id: number;
thankee_username: string;
};

type NotificationEventMetadata = {
message: string;
};
Expand All @@ -496,7 +507,8 @@ type EventMetadata =
| PinEventMetadata
| NotificationEventMetadata
| UserTrackPersonalRecommendationMetadata
| CritiqueBrainzReview;
| CritiqueBrainzReview
| ThanksMetadata;

type TimelineEvent = {
event_type: EventTypeT;
Expand Down
Loading