FEATURE: display text excerpts when scrolling on mobile
This commit is contained in:
parent
36a80871a3
commit
88a46be051
|
@ -728,6 +728,63 @@ export default RestModel.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
backfillExcerpts(streamPosition){
|
||||||
|
this._excerpts = this._excerpts || [];
|
||||||
|
const stream = this.get('stream');
|
||||||
|
|
||||||
|
if (this._excerpts.loading) {
|
||||||
|
return this._excerpts.loading.then(()=>{
|
||||||
|
if(!this._excerpts[stream[streamPosition]]) {
|
||||||
|
return this.backfillExcerpts(streamPosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let postIds = stream.slice(Math.max(streamPosition-20,0), streamPosition+20);
|
||||||
|
|
||||||
|
for(let i=postIds.length-1;i>=0;i--) {
|
||||||
|
if (this._excerpts[postIds[i]]) {
|
||||||
|
postIds.splice(i,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
post_ids: postIds
|
||||||
|
};
|
||||||
|
|
||||||
|
this._excerpts.loading = ajax("/t/" + this.get('topic.id') + "/excerpts.json", {data})
|
||||||
|
.then(excerpts => {
|
||||||
|
excerpts.forEach(obj => {
|
||||||
|
this._excerpts[obj.post_id] = obj;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(()=>{ this._excerpts.loading = null; });
|
||||||
|
|
||||||
|
return this._excerpts.loading;
|
||||||
|
},
|
||||||
|
|
||||||
|
excerpt(streamPosition){
|
||||||
|
|
||||||
|
const stream = this.get('stream');
|
||||||
|
|
||||||
|
return new Ember.RSVP.Promise((resolve,reject) => {
|
||||||
|
|
||||||
|
let excerpt = this._excerpts && this._excerpts[stream[streamPosition]];
|
||||||
|
|
||||||
|
if(excerpt) {
|
||||||
|
resolve(excerpt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.backfillExcerpts(streamPosition)
|
||||||
|
.then(()=>{
|
||||||
|
resolve(this._excerpts[stream[streamPosition]]);
|
||||||
|
})
|
||||||
|
.catch(e => reject(e));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
indexOf(post) {
|
indexOf(post) {
|
||||||
return this.get('stream').indexOf(post.get('id'));
|
return this.get('stream').indexOf(post.get('id'));
|
||||||
},
|
},
|
||||||
|
|
|
@ -131,6 +131,13 @@ createWidget('timeline-scrollarea', {
|
||||||
result.lastReadPercentage = this._percentFor(topic, idx);
|
result.lastReadPercentage = this._percentFor(topic, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (this.state.position !== result.current) {
|
||||||
|
this.state.position = result.current;
|
||||||
|
const timeline = this._findAncestorWithProperty('updatePosition');
|
||||||
|
timeline.updatePosition.call(timeline, result.current);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -219,6 +226,47 @@ createWidget('topic-timeline-container', {
|
||||||
export default createWidget('topic-timeline', {
|
export default createWidget('topic-timeline', {
|
||||||
tagName: 'div.topic-timeline',
|
tagName: 'div.topic-timeline',
|
||||||
|
|
||||||
|
buildKey: () => 'topic-timeline-area',
|
||||||
|
|
||||||
|
defaultState() {
|
||||||
|
return { position: null, excerpt: null };
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePosition(pos) {
|
||||||
|
if (!this.attrs.fullScreen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.position = pos;
|
||||||
|
this.state.excerpt = "";
|
||||||
|
this.scheduleRerender();
|
||||||
|
|
||||||
|
const stream = this.attrs.topic.get('postStream');
|
||||||
|
|
||||||
|
// a little debounce to avoid flashing
|
||||||
|
setTimeout(()=>{
|
||||||
|
if (!this.state.position === pos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.excerpt(pos).then(info => {
|
||||||
|
|
||||||
|
if (info && this.state.position === pos) {
|
||||||
|
let excerpt = "";
|
||||||
|
|
||||||
|
if (info.username) {
|
||||||
|
excerpt = "<span class='username'>" + info.username + ":</span> ";
|
||||||
|
}
|
||||||
|
|
||||||
|
excerpt += info.excerpt;
|
||||||
|
|
||||||
|
this.state.excerpt = excerpt;
|
||||||
|
this.scheduleRerender();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
|
||||||
html(attrs) {
|
html(attrs) {
|
||||||
const { topic } = attrs;
|
const { topic } = attrs;
|
||||||
const createdAt = new Date(topic.created_at);
|
const createdAt = new Date(topic.created_at);
|
||||||
|
@ -232,11 +280,21 @@ export default createWidget('topic-timeline', {
|
||||||
if (attrs.mobileView) {
|
if (attrs.mobileView) {
|
||||||
titleHTML = new RawHtml({ html: `<span>${topic.get('fancyTitle')}</span>` });
|
titleHTML = new RawHtml({ html: `<span>${topic.get('fancyTitle')}</span>` });
|
||||||
}
|
}
|
||||||
result.push(h('h3.title', this.attach('link', {
|
|
||||||
contents: ()=>titleHTML,
|
let elems = [h('h2', this.attach('link', {
|
||||||
className: 'fancy-title',
|
contents: ()=>titleHTML,
|
||||||
action: 'jumpTop'
|
className: 'fancy-title',
|
||||||
})));
|
action: 'jumpTop'}))];
|
||||||
|
|
||||||
|
|
||||||
|
if (this.state.excerpt) {
|
||||||
|
elems.push(
|
||||||
|
new RawHtml({
|
||||||
|
html: "<div class='post-excerpt'>" + this.state.excerpt + "</div>"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(h('div.title', elems));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
z-index: 100000;
|
z-index: 100000;
|
||||||
.topic-timeline {
|
.topic-timeline {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
display: table;
|
display: table;
|
||||||
|
@ -61,12 +62,38 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
.post-excerpt {
|
||||||
|
max-width: 650px;
|
||||||
|
max-height: 155px;
|
||||||
|
line-height: 1.4em;
|
||||||
|
overflow: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.username {
|
||||||
|
color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%));
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
line-height: 1.3em;
|
width: 100%;
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
display: block;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
max-height: 110px;
|
||||||
|
line-height: 1.3em;
|
||||||
|
word-wrap: break-word;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,6 +186,40 @@ class TopicsController < ApplicationController
|
||||||
render_json_dump(TopicViewPostsSerializer.new(@topic_view, scope: guardian, root: false, include_raw: !!params[:include_raw]))
|
render_json_dump(TopicViewPostsSerializer.new(@topic_view, scope: guardian, root: false, include_raw: !!params[:include_raw]))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def excerpts
|
||||||
|
params.require(:topic_id)
|
||||||
|
params.require(:post_ids)
|
||||||
|
|
||||||
|
post_ids = params[:post_ids].map(&:to_i)
|
||||||
|
unless Array === post_ids
|
||||||
|
render_json_error("Expecting post_ids to contain a list of posts ids")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if post_ids.length > 100
|
||||||
|
render_json_error("Requested a chunk that is too big")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@topic = Topic.with_deleted.where(id: params[:topic_id]).first
|
||||||
|
guardian.ensure_can_see!(@topic)
|
||||||
|
|
||||||
|
@posts = Post.where(hidden: false, deleted_at: nil, topic_id: @topic.id)
|
||||||
|
.where('posts.id in (?)', post_ids)
|
||||||
|
.joins("LEFT JOIN users u on u.id = posts.user_id")
|
||||||
|
.pluck(:id, :cooked, :username)
|
||||||
|
.map do |post_id, cooked, username|
|
||||||
|
{
|
||||||
|
post_id: post_id,
|
||||||
|
username: username,
|
||||||
|
excerpt: PrettyText.excerpt(cooked, 800, keep_emoji_images: true)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
render json: @posts.to_json
|
||||||
|
end
|
||||||
|
|
||||||
def destroy_timings
|
def destroy_timings
|
||||||
PostTiming.destroy_for(current_user.id, [params[:topic_id].to_i])
|
PostTiming.destroy_for(current_user.id, [params[:topic_id].to_i])
|
||||||
render nothing: true
|
render nothing: true
|
||||||
|
|
|
@ -592,6 +592,7 @@ Discourse::Application.routes.draw do
|
||||||
get "t/:slug/:topic_id/:post_number" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/}
|
get "t/:slug/:topic_id/:post_number" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/}
|
||||||
get "t/:slug/:topic_id/last" => "topics#show", post_number: 99999999, constraints: {topic_id: /\d+/}
|
get "t/:slug/:topic_id/last" => "topics#show", post_number: 99999999, constraints: {topic_id: /\d+/}
|
||||||
get "t/:topic_id/posts" => "topics#posts", constraints: {topic_id: /\d+/}, format: :json
|
get "t/:topic_id/posts" => "topics#posts", constraints: {topic_id: /\d+/}, format: :json
|
||||||
|
get "t/:topic_id/excerpts" => "topics#excerpts", constraints: {topic_id: /\d+/}, format: :json
|
||||||
post "t/:topic_id/timings" => "topics#timings", constraints: {topic_id: /\d+/}
|
post "t/:topic_id/timings" => "topics#timings", constraints: {topic_id: /\d+/}
|
||||||
post "t/:topic_id/invite" => "topics#invite", constraints: {topic_id: /\d+/}
|
post "t/:topic_id/invite" => "topics#invite", constraints: {topic_id: /\d+/}
|
||||||
post "t/:topic_id/invite-group" => "topics#invite_group", constraints: {topic_id: /\d+/}
|
post "t/:topic_id/invite-group" => "topics#invite_group", constraints: {topic_id: /\d+/}
|
||||||
|
|
|
@ -1306,6 +1306,36 @@ describe TopicsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "excerpts" do
|
||||||
|
|
||||||
|
it "can correctly get excerpts" do
|
||||||
|
|
||||||
|
first_post = create_post(raw: 'This is the first post :)', title: 'This is a test title I am making yay')
|
||||||
|
second_post = create_post(raw: 'This is second post', topic: first_post.topic)
|
||||||
|
|
||||||
|
random_post = Fabricate(:post)
|
||||||
|
|
||||||
|
|
||||||
|
xhr :get, :excerpts, topic_id: first_post.topic_id, post_ids: [first_post.id, second_post.id, random_post.id]
|
||||||
|
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json.sort!{|a,b| a["post_id"] <=> b["post_id"]}
|
||||||
|
|
||||||
|
# no random post
|
||||||
|
expect(json.length).to eq(2)
|
||||||
|
# keep emoji images
|
||||||
|
expect(json[0]["excerpt"]).to match(/emoji/)
|
||||||
|
expect(json[0]["excerpt"]).to match(/first post/)
|
||||||
|
expect(json[0]["username"]).to eq(first_post.user.username)
|
||||||
|
expect(json[0]["post_id"]).to eq(first_post.id)
|
||||||
|
|
||||||
|
expect(json[1]["excerpt"]).to match(/second post/)
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
context "convert_topic" do
|
context "convert_topic" do
|
||||||
it 'needs you to be logged in' do
|
it 'needs you to be logged in' do
|
||||||
expect { xhr :put, :convert_topic, id: 111, type: "private" }.to raise_error(Discourse::NotLoggedIn)
|
expect { xhr :put, :convert_topic, id: 111, type: "private" }.to raise_error(Discourse::NotLoggedIn)
|
||||||
|
|
Loading…
Reference in New Issue