-
Notifications
You must be signed in to change notification settings - Fork 80
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
Develop a hosting feature so streamers can recommend other channels to their community when they are offline. #786
Changes from 1 commit
6df6d1e
95dcb5f
36049dc
720c1f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
@mixin show-dropdown { | ||
z-index: 9999; | ||
display: block; | ||
max-height: 250px; | ||
overflow: auto; | ||
} | ||
|
||
.channel-typeahead { | ||
position: relative; | ||
&-dropdown { | ||
display: none; | ||
position: absolute; | ||
top: 100%; | ||
left: 0; | ||
width: 100%; | ||
margin-top: -2px; | ||
> .list-group-item { | ||
display: flex; | ||
align-items: center; | ||
padding: 12px 15px; | ||
margin: 0; | ||
opacity: 100; | ||
&:hover { | ||
background: blue; | ||
cursor: pointer; | ||
} | ||
} | ||
&:hover { | ||
@include show-dropdown; | ||
} | ||
} | ||
&-input { | ||
position: relative; | ||
&:focus { | ||
+ .channel-typeahead-dropdown { | ||
@include show-dropdown; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
.fixed-header-table { | ||
table { | ||
text-align: left; | ||
position: relative; | ||
border-collapse: collapse; | ||
} | ||
th, td { | ||
padding: 0.25rem; | ||
} | ||
th { | ||
position: sticky; | ||
top: 0; /* Don't forget this, required for the stickiness */ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export default { | ||
mounted() { | ||
this.el.addEventListener("click", e => { | ||
let userId = this.el.getAttribute('data-id'); | ||
let userName = this.el.getAttribute('data-name'); | ||
let fieldName = this.el.getAttribute('data-fieldname'); | ||
let channelId = this.el.getAttribute('data-channel-id'); | ||
this.pushEvent(fieldName + "_selection_made", {"user_id": userId, "username": userName, "channel_id": channelId}); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
defmodule Glimesh.ChannelHostsLookups do | ||
@moduledoc """ | ||
Channel Hosting lookups | ||
""" | ||
|
||
import Ecto.Query, warn: false | ||
|
||
alias Glimesh.ChannelLookups | ||
alias Glimesh.Repo | ||
alias Glimesh.Streams.ChannelHosts | ||
|
||
def get_channel_hosting_list(channel_id) do | ||
ChannelHosts | ||
|> where([ch], ch.hosting_channel_id == ^channel_id) | ||
|> preload(host: [:user], target: [:user]) | ||
|> Repo.all() | ||
end | ||
|
||
def get_current_hosting_target(channel) do | ||
ChannelHosts | ||
|> where([ch], ch.hosting_channel_id == ^channel.id and ch.status == "hosting") | ||
|> limit(1) | ||
|> preload(host: [:user], target: [:user]) | ||
|> Repo.one() | ||
end | ||
|
||
def get_targets_host_info(host_username, target_channel) do | ||
host = ChannelLookups.get_channel_for_username(host_username) | ||
|
||
ChannelHosts | ||
|> where( | ||
[ch], | ||
ch.hosting_channel_id == ^host.id and ch.status == "hosting" and | ||
ch.target_channel_id == ^target_channel.id | ||
) | ||
|> preload(host: [:user], target: [:user]) | ||
|> Repo.one() | ||
end | ||
|
||
@doc """ | ||
Used by Auto Host Job -- find channels that are being hosted but are no longer live and reset them to "ready" state | ||
""" | ||
def unhost_channels_not_live do | ||
sql = """ | ||
update channel_hosts\ | ||
set status = 'ready'\ | ||
where id in\ | ||
(select ch.id from channel_hosts as ch\ | ||
inner join channels as c on ch.target_channel_id = c.id\ | ||
where c.status != 'live'\ | ||
and ch.status = 'hosting')\ | ||
returning id\ | ||
""" | ||
|
||
Repo.query(sql) | ||
end | ||
|
||
@doc """ | ||
Used by Auto Host Job -- find channels that are no longer eligible for hosting by the current host and set them to "error" state | ||
""" | ||
def invalidate_hosting_channels_where_necessary do | ||
sql = """ | ||
update channel_hosts\ | ||
set status = 'error'\ | ||
where id in\ | ||
(select ch.id from channel_hosts as ch\ | ||
inner join channels as tc on ch.target_channel_id = tc.id\ | ||
inner join users as tu on tc.user_id = tu.id\ | ||
inner join channels as hc on ch.hosting_channel_id = hc.id\ | ||
inner join users as hu on hc.user_id = hu.id\ | ||
where ch.status != 'error'\ | ||
and (tc.allow_hosting = 'false'\ | ||
or tc.inaccessible = 'true'\ | ||
or tu.is_banned = 'true'\ | ||
or tu.can_stream = 'false'\ | ||
or exists(select user_id from channel_bans where user_id = hc.user_id and channel_id = tc.id and expires_at is null)\ | ||
or hu.is_banned = 'true'\ | ||
or hu.can_stream = 'false'\ | ||
or hc.inaccessible = 'true'))\ | ||
returning id\ | ||
""" | ||
|
||
Repo.query(sql) | ||
end | ||
|
||
@doc """ | ||
Used by Auto Host Job -- find channels that are live with hosted channels that aren't live and host them! | ||
This will only affect channel_hosts table records in "ready" state -- it is assumed that invalidate_hosting_channels_where_necessary() | ||
and unhost_channels_not_live() have updated the channel_hosts table record states appropriately. | ||
""" | ||
def host_some_channels do | ||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) | ||
|
||
sql = """ | ||
update channel_hosts\ | ||
set status = 'hosting', last_hosted_date = $1\ | ||
where id in\ | ||
(select distinct on(ch_sub.hosting_channel_id) ch_sub.id\ | ||
from channel_hosts as ch_sub\ | ||
inner join channels as tc on ch_sub.target_channel_id = tc.id\ | ||
inner join channels as hc on ch_sub.hosting_channel_id = hc.id\ | ||
where ch_sub.status = 'ready'\ | ||
and hc.status = 'offline'\ | ||
and tc.status = 'live'\ | ||
and not exists(select true from channel_hosts where status = 'hosting' and hosting_channel_id = ch_sub.hosting_channel_id)\ | ||
group by ch_sub.id\ | ||
order by ch_sub.hosting_channel_id asc, min(coalesce(ch_sub.last_hosted_date, to_timestamp(0))) asc)\ | ||
returning id\ | ||
""" | ||
|
||
Repo.query(sql, [now]) | ||
end | ||
|
||
def recheck_error_status_channels do | ||
sql = """ | ||
update channel_hosts\ | ||
set status = 'ready'\ | ||
where id in\ | ||
(select ch.id from channel_hosts as ch\ | ||
inner join channels as tc on ch.target_channel_id = tc.id\ | ||
inner join users as tu on tc.user_id = tu.id\ | ||
inner join channels as hc on ch.hosting_channel_id = hc.id\ | ||
inner join users as hu on hc.user_id = hu.id\ | ||
where ch.status = 'error'\ | ||
and tc.allow_hosting = 'true'\ | ||
and tc.inaccessible = 'false'\ | ||
and tu.is_banned = 'false'\ | ||
and tu.can_stream = 'true'\ | ||
and not exists(select user_id from channel_bans where user_id = hc.user_id and channel_id = tc.id and expires_at is null)\ | ||
and hu.is_banned = 'false'\ | ||
and hu.can_stream = 'true'\ | ||
and hc.inaccessible = 'false')\ | ||
returning id\ | ||
""" | ||
|
||
Repo.query(sql) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ defmodule Glimesh.ChannelLookups do | |
alias Glimesh.AccountFollows.Follower | ||
alias Glimesh.Accounts.User | ||
alias Glimesh.Repo | ||
alias Glimesh.Streams.{Category, Channel} | ||
alias Glimesh.Streams.{Category, Channel, ChannelHosts} | ||
|
||
## Filtering | ||
@spec search_live_channels(map) :: list | ||
|
@@ -113,6 +113,62 @@ defmodule Glimesh.ChannelLookups do | |
|> Repo.preload([:category, :user, :stream, :subcategory, :tags]) | ||
end | ||
|
||
def count_live_followed_channels_that_are_hosting(%User{} = user) do | ||
from(c in Channel, | ||
left_join: f in Follower, | ||
on: c.user_id == f.streamer_id, | ||
join: ch in ChannelHosts, | ||
on: c.id == ch.hosting_channel_id, | ||
join: target in Channel, | ||
on: target.id == ch.target_channel_id, | ||
left_join: target_followers in Follower, | ||
on: target.user_id == target_followers.streamer_id, | ||
where: ch.status == "hosting", | ||
where: target.status == "live", | ||
where: f.user_id == ^user.id, | ||
where: target_followers.user_id != ^user.id or is_nil(target_followers.user_id), | ||
distinct: target.id, | ||
select: [target] | ||
) | ||
|> Repo.all() | ||
|> length() | ||
end | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've noted the following method in my "Performance Considerations" section. This query mostly utilizes joins on indexes but I have not been able to test it with a large dataset. |
||
def list_live_followed_channels_and_hosts(%User{} = user) do | ||
include_hosts_query = | ||
from(c in Channel, | ||
left_join: f in Follower, | ||
on: c.user_id == f.streamer_id, | ||
join: ch in ChannelHosts, | ||
on: c.id == ch.hosting_channel_id, | ||
join: target in Channel, | ||
on: target.id == ch.target_channel_id, | ||
left_join: target_followers in Follower, | ||
on: target.user_id == target_followers.streamer_id, | ||
where: ch.status == "hosting", | ||
where: target.status == "live", | ||
where: f.user_id == ^user.id, | ||
where: target_followers.user_id != ^user.id or is_nil(target_followers.user_id), | ||
distinct: target.id, | ||
select: [target], | ||
select_merge: %{match_type: "hosting"} | ||
) | ||
|
||
live_followed_query = | ||
from([c] in Channel, | ||
join: f in Follower, | ||
on: c.user_id == f.streamer_id, | ||
where: c.status == "live", | ||
where: f.user_id == ^user.id, | ||
select_merge: %{match_type: "live"} | ||
) | ||
|
||
query = live_followed_query |> union_all(^include_hosts_query) | ||
|
||
Repo.all(query) | ||
|> Repo.preload([:user, :category, :stream, :subcategory, :tags]) | ||
end | ||
|
||
def list_all_followed_channels(user) do | ||
Repo.all( | ||
from c in Channel, | ||
|
@@ -202,4 +258,38 @@ defmodule Glimesh.ChannelLookups do | |
def get_any_channel_for_user(user) do | ||
Repo.one(from c in Channel, where: c.user_id == ^user.id) | ||
end | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The following method is used by the add hosts text input box type-ahead code. Since it takes raw user input, I attempt to sanitize that input against '\', '_', and '%' characters. |
||
def search_hostable_channels_by_name(hosting_user, target_name) do | ||
if target_name != nil and String.length(target_name) < 25 do | ||
search_term = Regex.replace(~r/(\\\\|_|%)/, target_name, "\\\\\\1") <> "%" | ||
|
||
Repo.all( | ||
from c in Channel, | ||
join: u in User, | ||
on: c.user_id == u.id, | ||
where: ilike(u.displayname, ^search_term), | ||
where: c.allow_hosting == true, | ||
where: c.inaccessible == false, | ||
where: c.user_id != ^hosting_user.id, | ||
where: u.is_banned == false, | ||
where: u.can_stream == true, | ||
where: | ||
fragment( | ||
"not exists(select user_id from channel_bans where user_id = ? and channel_id = ? and expires_at is null)", | ||
^hosting_user.id, | ||
c.id | ||
), | ||
where: | ||
fragment( | ||
"not exists(select target_channel_id from channel_hosts where target_channel_id = ?)", | ||
c.id | ||
), | ||
order_by: [asc: u.displayname], | ||
limit: 10 | ||
) | ||
|> Repo.preload([:user]) | ||
else | ||
[] | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used raw sql in the following methods for clarity purposes. The Ecto.Query format was getting hard to read.