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 9 commits into
base: master
Choose a base branch
from
Draft
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
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"
}
90 changes: 74 additions & 16 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 @@ -62,6 +63,7 @@ export enum EventType {
BLOCK_FOLLOW = "block_follow",
NOTIFICATION = "notification",
REVIEW = "critiquebrainz_review",
THANKS = "thanks",
}

export type UserFeedPageProps = {
Expand All @@ -86,7 +88,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 +115,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 +153,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 +264,33 @@ export default function UserFeedPage() {
},
[APIService, currentUser]
);

const thankFeedEvent = React.useCallback(
async (event: TimelineEvent) => {
const { thankFeedEvent } = APIService;
try {
const status = await thankFeedEvent(
event.event_type,
currentUser.name,
currentUser.auth_token as string,
event.id!,
event.metadata?.blurb_content
);

if (status === 200) {
return event;
}
} catch (error) {
toast.error(
<ToastMsg title="Could not thank event" message={error.toString()} />,
{ toastId: "hide-error" }
);
}
return undefined;
},
[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 @@ -411,29 +445,53 @@ export default function UserFeedPage() {
) {
if (event.hidden) {
return (
<>
<ListenControl
title="Thank Event"
text=""
icon={faHandshake}
buttonClassName="btn btn-link btn-xs"
// eslint-disable-next-line react/jsx-no-bind
action={() => {
thankFeedEvent(event);
}}
/>
<ListenControl
title="Unhide Event"
text=""
icon={faEye}
buttonClassName="btn btn-link btn-xs"
// eslint-disable-next-line react/jsx-no-bind
action={() => {
hideEventMutation(event);
}}
/>
</>
);
}
return (
<>
<ListenControl
title="Thank Event"
text=""
icon={faHandshake}
buttonClassName="btn btn-link btn-xs"
// eslint-disable-next-line react/jsx-no-bind
action={() => {
thankFeedEvent(event);
}}
/>
<ListenControl
title="Unhide Event"
title="Hide Event"
text=""
icon={faEye}
icon={faEyeSlash}
buttonClassName="btn btn-link btn-xs"
// eslint-disable-next-line react/jsx-no-bind
action={() => {
hideEventMutation(event);
}}
/>
);
}
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);
}}
/>
</>
);
}
return null;
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 (
eventType: string,
username: string,
userToken: string,
event_id: number,
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
10 changes: 8 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,10 @@ type UserRelationshipEventMetadata = {
created: number;
};

type ThanksMetadata = {
blurb_content: string;
};

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

type TimelineEvent = {
event_type: EventTypeT;
Expand Down
8 changes: 8 additions & 0 deletions listenbrainz/db/model/user_timeline_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class UserTimelineEventType(Enum):
RECORDING_PIN = 'recording_pin'
CRITIQUEBRAINZ_REVIEW = 'critiquebrainz_review'
PERSONAL_RECORDING_RECOMMENDATION = 'personal_recording_recommendation'
THANKS = 'thanks'


class RecordingRecommendationMetadata(MsidMbidModel):
Expand All @@ -59,6 +60,13 @@ class NotificationMetadata(BaseModel):
message: constr(min_length=1)


class ThanksMetadata(MsidMbidModel):
original_event_id: NonNegativeInt
original_event_type: UserTimelineEventType
blurb_content: Optional[str]



UserTimelineEventMetadata = Union[CBReviewTimelineMetadata, PersonalRecordingRecommendationMetadata,
RecordingRecommendationMetadata, NotificationMetadata]

Expand Down
44 changes: 44 additions & 0 deletions listenbrainz/db/user_timeline_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
UserTimelineEvent,
UserTimelineEventType,
UserTimelineEventMetadata,
ThanksMetadata,
RecordingRecommendationMetadata,
NotificationMetadata,
HiddenUserTimelineEvent,
Expand Down Expand Up @@ -86,6 +87,17 @@ def create_user_notification_event(db_conn, user_id: int, metadata: Notification
)


def create_thanks_event(db_conn, user_id: int, metadata: ThanksMetadata) -> UserTimelineEvent:
""" Creates a thanks event in the database and returns it.
"""
return create_user_timeline_event(
db_conn,
user_id=user_id,
event_type=UserTimelineEventType.THANKS,
metadata=metadata,
)


def delete_user_timeline_event(db_conn, id: int, user_id: int) -> bool:
""" Deletes recommendation and notification event using id """
try:
Expand Down Expand Up @@ -153,6 +165,38 @@ def create_personal_recommendation_event(db_conn, user_id: int, metadata: WriteP
raise DatabaseException(str(e))


def create_thanks_event(db_conn, user_id: int, metadata: ThanksMetadata)\
-> UserTimelineEvent:
""" Creates a thanks event in the database and returns it.
The User ID in the table is the person thanking meanwhile the original_event_id and original_event_type refer to
the event id and type being thanked respectively.
"""
try:
result = db_conn.execute(text("""
INSERT INTO user_timeline_event (user_id, event_type, metadata)
VALUES (
:user_id,
'thanks',
jsonb_build_object(
'original_event_id', :original_event_id,
'original_event_type', :original_event_type,
'blurb_content', :blurb_content
)
)
RETURNING id, user_id, event_type, metadata, created
"""), {
'user_id': user_id,
'original_event_id': metadata.original_event_id,
'original_event_type': metadata.original_event_type,
'blurb_content': metadata.blurb_content
}
)
db_conn.commit()
return UserTimelineEvent(**result.mappings().first())
except Exception as e:
raise DatabaseException(str(e))


def get_user_timeline_events(
db_conn,
user_ids: Iterable[int],
Expand Down
Loading