Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type PostTextUpdateArgs = {
body: string
mint: string
isMembersOnly?: boolean
videoUrl?: string
}

export const usePostTextUpdate = () => {
Expand All @@ -31,12 +32,13 @@ export const usePostTextUpdate = () => {
entityType: 'FanClub',
body: args.body,
mentions: [],
isMembersOnly: args.isMembersOnly ?? true
isMembersOnly: args.isMembersOnly ?? true,
videoUrl: args.videoUrl
} as any
})
},
onMutate: async (args: PostTextUpdateArgs & { newId?: ID }) => {
const { userId, body, entityId, mint, isMembersOnly } = args
const { userId, body, entityId, mint, isMembersOnly, videoUrl } = args
const sdk = await audiusSdk()
const newId = await sdk.comments.generateCommentId()
args.newId = newId
Expand All @@ -55,7 +57,8 @@ export const usePostTextUpdate = () => {
replies: undefined,
createdAt: new Date().toISOString(),
updatedAt: undefined,
isMembersOnly: isMembersOnly ?? true
isMembersOnly: isMembersOnly ?? true,
videoUrl
}

// Prime the individual comment cache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const primeCommentData = ({
}) => {
// Populate individual comment cache
comments.forEach((comment) => {
// Prime the main comment
queryClient.setQueryData(getCommentQueryKey(comment.id), comment)

// Prime any replies if they exist
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ export * from './quickSearch'
export * from './coinMetrics'
export * from './convertHexToRGBA'
export * from './socialLinks'
export * from './videoUtils'
69 changes: 69 additions & 0 deletions packages/common/src/utils/videoUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export type VideoPlatform = 'youtube' | 'vimeo'

export type ParsedVideo = {
platform: VideoPlatform
videoId: string
}

const YOUTUBE_REGEX =
/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/

const VIMEO_REGEX = /(?:vimeo\.com\/)(\d+)/

/**
* Parse a video URL and extract the platform and video ID.
* Supports YouTube and Vimeo URLs.
*/
export const parseVideoUrl = (url: string): ParsedVideo | null => {
const youtubeMatch = url.match(YOUTUBE_REGEX)
if (youtubeMatch) {
return { platform: 'youtube', videoId: youtubeMatch[1] }
}

const vimeoMatch = url.match(VIMEO_REGEX)
if (vimeoMatch) {
return { platform: 'vimeo', videoId: vimeoMatch[1] }
}

return null
}

/**
* Get the thumbnail URL for a video. Only YouTube provides static thumbnail URLs.
* Vimeo requires an API call, so returns null.
*/
export const getVideoThumbnailUrl = (
parsed: ParsedVideo
): string | null => {
if (parsed.platform === 'youtube') {
return `https://img.youtube.com/vi/${parsed.videoId}/hqdefault.jpg`
}
return null
}

/**
* Get the embeddable URL for a video.
*/
export const getVideoEmbedUrl = (parsed: ParsedVideo): string => {
if (parsed.platform === 'youtube') {
return `https://www.youtube.com/embed/${parsed.videoId}`
}
return `https://player.vimeo.com/video/${parsed.videoId}`
}

/**
* Get the watch URL for a video (for opening in a new tab).
*/
export const getVideoWatchUrl = (parsed: ParsedVideo): string => {
if (parsed.platform === 'youtube') {
return `https://www.youtube.com/watch?v=${parsed.videoId}`
}
return `https://vimeo.com/${parsed.videoId}`
}

/**
* Check if a URL is a valid YouTube or Vimeo video URL.
*/
export const isValidVideoUrl = (url: string): boolean => {
return parseVideoUrl(url) !== null
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import List

from sqlalchemy import text
from web3 import Web3
from web3.datastructures import AttributeDict

from integration_tests.challenges.index_helpers import UpdateTask
Expand All @@ -22,8 +23,6 @@
from src.tasks.entity_manager.entity_manager import entity_manager_update
from src.utils.db_session import get_db

from web3 import Web3

logger = logging.getLogger(__name__)

# Base entities shared across fan club post (text post) tests.
Expand Down Expand Up @@ -158,9 +157,9 @@ def test_create_fan_club_post(app, mocker):
assert comments[0].entity_id == 1
assert comments[0].user_id == 1

notifications = session.query(Notification).filter(
Notification.type == "comment"
).all()
notifications = (
session.query(Notification).filter(Notification.type == "comment").all()
)
assert len(notifications) == 0


Expand All @@ -169,12 +168,14 @@ def test_create_fan_club_post_invalid_no_artist_coin(app, mocker):
Creating a fan club post for a user_id that has no artist coin should be
silently skipped (validation error).
"""
bad_metadata = json.dumps({
"entity_id": 99, # user 99 doesn't exist and has no coin
"entity_type": "FanClub",
"body": "should fail",
"parent_comment_id": None,
})
bad_metadata = json.dumps(
{
"entity_id": 99, # user 99 doesn't exist and has no coin
"entity_type": "FanClub",
"body": "should fail",
"parent_comment_id": None,
}
)

tx_receipts = {
"BadFanClubPost": [
Expand Down Expand Up @@ -223,11 +224,13 @@ def test_fan_club_post_reply(app, mocker):
],
}

reply_metadata = json.dumps({
**fan_club_post_metadata,
"body": "replying to your post",
"parent_comment_id": 1,
})
reply_metadata = json.dumps(
{
**fan_club_post_metadata,
"body": "replying to your post",
"parent_comment_id": 1,
}
)

tx_receipts = {
"FanClubPostReply": [
Expand Down Expand Up @@ -283,12 +286,14 @@ def test_fan_club_post_reply_non_owner_rejected(app, mocker):
],
}

cross_thread_metadata = json.dumps({
"entity_id": 1,
"entity_type": "FanClub",
"body": "cross-thread reply",
"parent_comment_id": 1, # parent is a Track comment, not fan club
})
cross_thread_metadata = json.dumps(
{
"entity_id": 1,
"entity_type": "FanClub",
"body": "cross-thread reply",
"parent_comment_id": 1, # parent is a Track comment, not fan club
}
)

tx_receipts = {
"CrossThreadReply": [
Expand Down Expand Up @@ -537,11 +542,13 @@ def test_update_fan_club_post(app, mocker):
],
}

update_metadata = json.dumps({
"entity_id": 1,
"entity_type": "FanClub",
"body": "edited text",
})
update_metadata = json.dumps(
{
"entity_id": 1,
"entity_type": "FanClub",
"body": "edited text",
}
)

tx_receipts = {
"EditFanClubPost": [
Expand Down Expand Up @@ -578,11 +585,13 @@ def test_update_fan_club_post(app, mocker):

def test_fan_club_post_with_mention(app, mocker):
"""Mentions in a fan club text post generate mention notifications."""
mention_metadata = json.dumps({
**fan_club_post_metadata,
"body": "hey @user3 check this out",
"mentions": [3],
})
mention_metadata = json.dumps(
{
**fan_club_post_metadata,
"body": "hey @user3 check this out",
"mentions": [3],
}
)

tx_receipts = {
"FanClubPostMention": [
Expand Down Expand Up @@ -629,12 +638,14 @@ def test_fan_club_post_and_track_comments_coexist(app, mocker):
Creating both a fan club post and a Track comment in the same block
produces the correct entity_type on each and independent notifications.
"""
track_comment_metadata = json.dumps({
"entity_id": 1,
"entity_type": "Track",
"body": "great track!",
"parent_comment_id": None,
})
track_comment_metadata = json.dumps(
{
"entity_id": 1,
"entity_type": "Track",
"body": "great track!",
"parent_comment_id": None,
}
)

tx_receipts = {
"FanClubPost": [
Expand Down Expand Up @@ -684,9 +695,9 @@ def test_fan_club_post_and_track_comments_coexist(app, mocker):
assert track_comment.entity_id == 1

# Fan club owner self-post does not notify; track comment notifies track owner.
notifs = session.query(Notification).filter(
Notification.type == "comment"
).all()
notifs = (
session.query(Notification).filter(Notification.type == "comment").all()
)
assert len(notifs) == 1
assert notifs[0].group_id == "comment:1:type:Track"

Expand All @@ -712,7 +723,9 @@ def _seed_coin_holders(session, artist_user_id=1, holder_user_ids=None, mint=COI
session.flush()


def test_fan_club_text_post_notification_sent_to_followers_and_coin_holders(app, mocker):
def test_fan_club_text_post_notification_sent_to_followers_and_coin_holders(
app, mocker
):
"""
When an artist creates a root-level fan club text post, both followers
and coin holders receive a fan_club_text_post notification (deduplicated).
Expand Down Expand Up @@ -767,9 +780,7 @@ def test_fan_club_text_post_notification_sent_to_followers_and_coin_holders(app,

# No "comment" notification since artist is posting on their own fan club
comment_notifs = (
session.query(Notification)
.filter(Notification.type == "comment")
.all()
session.query(Notification).filter(Notification.type == "comment").all()
)
assert len(comment_notifs) == 0

Expand Down Expand Up @@ -835,11 +846,13 @@ def test_fan_club_text_post_notification_not_sent_for_replies(app, mocker):
],
}

reply_metadata = json.dumps({
**fan_club_post_metadata,
"body": "replying to my own post",
"parent_comment_id": 1,
})
reply_metadata = json.dumps(
{
**fan_club_post_metadata,
"body": "replying to my own post",
"parent_comment_id": 1,
}
)

tx_receipts = {
"ArtistReply": [
Expand Down
2 changes: 2 additions & 0 deletions packages/discovery-provider/src/api/v1/models/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"is_artist_reacted": fields.Boolean(required=False),
"created_at": fields.String(required=True),
"updated_at": fields.String(required=False),
"video_url": fields.String(required=False),
},
)

Expand Down Expand Up @@ -55,6 +56,7 @@
"created_at": fields.String(required=True),
"updated_at": fields.String(required=False),
"replies": fields.List(fields.Nested(reply_comment_model), require=True),
"video_url": fields.String(required=False),
},
)

Expand Down
1 change: 1 addition & 0 deletions packages/discovery-provider/src/models/comments/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Comment(Base, RepresentableMixin):
is_visible = Column(Boolean, default=True)
is_edited = Column(Boolean, default=False)
is_members_only = Column(Boolean, default=False, nullable=False)
video_url = Column(Text, nullable=True)
txhash = Column(Text, nullable=False)
blockhash = Column(Text, nullable=False)
blocknumber = Column(Integer, ForeignKey("blocks.number"), nullable=False)
Expand Down
4 changes: 3 additions & 1 deletion packages/discovery-provider/src/queries/comments/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

from sqlalchemy import func

from src.models.comments.comment import FAN_CLUB_ENTITY_TYPE
from src.models.comments.comment_mention import CommentMention
from src.models.comments.comment_notification_setting import CommentNotificationSetting
from src.models.comments.comment_reaction import CommentReaction
from src.models.comments.comment_report import COMMENT_KARMA_THRESHOLD
from src.models.moderation.muted_user import MutedUser
from src.models.comments.comment import FAN_CLUB_ENTITY_TYPE
from src.models.users.aggregate_user import AggregateUser
from src.models.users.user import User
from src.queries.query_helpers import get_tracks, get_users
Expand Down Expand Up @@ -145,6 +145,7 @@ def is_reacted(user_id, comment_id):
"created_at": str(comment.created_at),
"updated_at": str(comment.updated_at),
"is_muted": is_muted if is_muted is not None else False,
"video_url": getattr(comment, "video_url", None),
}

# Check if we need to include replies (either explicitly provided or need to fetch them)
Expand Down Expand Up @@ -878,6 +879,7 @@ def get_comment_replies(
"created_at": str(reply.created_at),
"updated_at": str(reply.updated_at),
"is_muted": False, # Replies don't have mute status
"video_url": getattr(reply, "video_url", None),
"is_artist_reacted": (
reactions_map.get((artist_id, reply.comment_id), False)
if reactions_map
Expand Down
Loading
Loading