- Add extensive test coverage for Bluesky user profile URL matching - Test handle-based and DID-based profile URLs with various formats - Add edge cases and error condition tests for malformed URLs - Test user avatar icon path and model path generation - Verify fallback behavior for users without display names - Test priority logic for handle vs DID lookup - Add tests for special characters and very long handles - All 82 tests now pass successfully
211 lines
5.4 KiB
Ruby
211 lines
5.4 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
module Domain::BlueskyPostHelper
|
|
extend T::Sig
|
|
include ActionView::Helpers::UrlHelper
|
|
include HelpersInterface
|
|
include Domain::PostsHelper
|
|
|
|
class FacetPart < T::Struct
|
|
const :type, Symbol
|
|
const :value, String
|
|
end
|
|
|
|
sig do
|
|
params(text: String, facets: T.nilable(T::Array[T.untyped])).returns(
|
|
T.nilable(String),
|
|
)
|
|
end
|
|
def render_bsky_post_facets(text, facets = nil)
|
|
return text if facets.blank?
|
|
|
|
facets =
|
|
begin
|
|
facets.map { |facet| Bluesky::Text::Facet.from_hash(facet) }
|
|
rescue => e
|
|
Rails.logger.error("error parsing Bluesky facets: #{e.message}")
|
|
return text
|
|
end
|
|
|
|
result_parts = T.let([], T::Array[FacetPart])
|
|
last_end = 0
|
|
|
|
# Sort facets by start position to handle them in order
|
|
sorted_facets = facets.sort_by(&:byteStart)
|
|
|
|
sorted_facets.each do |facet|
|
|
if facet.byteStart < 0 || facet.byteEnd <= facet.byteStart ||
|
|
facet.byteEnd > text.bytesize
|
|
next
|
|
end
|
|
|
|
# Skip overlapping facets
|
|
next if facet.byteStart < last_end
|
|
|
|
# Add text before this facet
|
|
if facet.byteStart > last_end
|
|
before_text = text.byteslice(last_end, facet.byteStart - last_end)
|
|
if before_text
|
|
result_parts << FacetPart.new(type: :text, value: before_text)
|
|
end
|
|
end
|
|
|
|
# Extract the facet text using byteslice for accurate character extraction
|
|
facet_text =
|
|
text.byteslice(facet.byteStart, facet.byteEnd - facet.byteStart)
|
|
next unless facet_text # Skip if byteslice returns nil
|
|
|
|
# Process the facet
|
|
rendered_facet = render_facet(facet, facet_text)
|
|
result_parts << FacetPart.new(type: :facet, value: rendered_facet)
|
|
|
|
last_end = facet.byteEnd
|
|
end
|
|
|
|
# Add remaining text after the last facet
|
|
if last_end < text.bytesize
|
|
remaining_text = text.byteslice(last_end, text.bytesize - last_end)
|
|
if remaining_text
|
|
result_parts << FacetPart.new(type: :text, value: remaining_text)
|
|
end
|
|
end
|
|
|
|
result_parts
|
|
.map do |part|
|
|
case part.type
|
|
when :text
|
|
part.value.gsub("\n", "<br />")
|
|
when :facet
|
|
part.value
|
|
end
|
|
end
|
|
.join
|
|
.html_safe
|
|
end
|
|
|
|
private
|
|
|
|
sig do
|
|
params(facet: Bluesky::Text::Facet, facet_text: String).returns(String)
|
|
end
|
|
def render_facet(facet, facet_text)
|
|
return facet_text unless facet.features.any?
|
|
|
|
# Process the first feature (Bluesky facets typically have one feature per facet)
|
|
feature = facet.features.first
|
|
return facet_text unless feature.is_a?(Bluesky::Text::FacetFeature)
|
|
|
|
case feature
|
|
when Bluesky::Text::FacetFeatureMention
|
|
render_mention_facet(feature, facet_text)
|
|
when Bluesky::Text::FacetFeatureURI
|
|
render_link_facet(feature, facet_text)
|
|
when Bluesky::Text::FacetFeatureTag
|
|
render_tag_facet(feature, facet_text)
|
|
else
|
|
# Unknown facet type, return original text
|
|
facet_text
|
|
end
|
|
end
|
|
|
|
sig do
|
|
params(
|
|
feature: Bluesky::Text::FacetFeatureMention,
|
|
facet_text: String,
|
|
).returns(String)
|
|
end
|
|
def render_mention_facet(feature, facet_text)
|
|
did = feature.did
|
|
return facet_text unless did.present?
|
|
|
|
# Try to find the user in the database
|
|
user = Domain::User::BlueskyUser.find_by(did: did)
|
|
|
|
if user
|
|
# Render the inline user partial
|
|
render(
|
|
partial: "domain/has_description_html/inline_link_domain_user",
|
|
locals: {
|
|
user: user,
|
|
link_text: facet_text,
|
|
visual_style: "description-section-link-light",
|
|
},
|
|
)
|
|
else
|
|
# Render external link to Bluesky profile
|
|
render(
|
|
partial: "domain/has_description_html/external_link",
|
|
locals: {
|
|
link_text: facet_text,
|
|
url: "https://bsky.app/profile/#{did}",
|
|
},
|
|
)
|
|
end
|
|
end
|
|
|
|
sig do
|
|
params(feature: Bluesky::Text::FacetFeatureURI, facet_text: String).returns(
|
|
String,
|
|
)
|
|
end
|
|
def render_link_facet(feature, facet_text)
|
|
uri = feature.uri
|
|
return facet_text unless uri.present?
|
|
|
|
source = link_for_source(uri)
|
|
if source.present? && (model = source.model)
|
|
case model
|
|
when Domain::Post
|
|
return(
|
|
render(
|
|
partial: "domain/has_description_html/inline_link_domain_post",
|
|
locals: {
|
|
post: model,
|
|
link_text: facet_text,
|
|
visual_style: "description-section-link-light",
|
|
},
|
|
)
|
|
)
|
|
when Domain::User
|
|
return(
|
|
render(
|
|
partial: "domain/has_description_html/inline_link_domain_user",
|
|
locals: {
|
|
user: model,
|
|
link_text: facet_text,
|
|
visual_style: "description-section-link-light",
|
|
},
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
render(
|
|
partial: "domain/has_description_html/external_link",
|
|
locals: {
|
|
link_text: facet_text,
|
|
url: uri,
|
|
},
|
|
)
|
|
end
|
|
|
|
sig do
|
|
params(feature: Bluesky::Text::FacetFeatureTag, facet_text: String).returns(
|
|
String,
|
|
)
|
|
end
|
|
def render_tag_facet(feature, facet_text)
|
|
tag = feature.tag
|
|
return facet_text unless tag.present?
|
|
|
|
render(
|
|
partial: "domain/has_description_html/external_link",
|
|
locals: {
|
|
link_text: facet_text,
|
|
url: "https://bsky.app/hashtag/#{tag}",
|
|
},
|
|
)
|
|
end
|
|
end
|