##// END OF EJS Templates
Run thread update when connecting to websocket to get missed posts if the...
neko259 -
r892:6155490b default
parent child Browse files
Show More
@@ -1,285 +1,339 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 var wsUrl = 'ws://localhost:9090/connection/websocket';
27 27 var wsUser = '';
28 28
29 29 var loading = false;
30 30 var lastUpdateTime = null;
31 31 var unreadPosts = 0;
32 var documentOriginalTitle = '';
32 33
33 34 // Thread ID does not change, can be stored one time
34 35 var threadId = $('div.thread').children('.post').first().attr('id');
35 36
36 37 function connectWebsocket() {
37 38 var metapanel = $('.metapanel')[0];
38 39
39 40 var wsHost = metapanel.getAttribute('data-ws-host');
40 41 var wsPort = metapanel.getAttribute('data-ws-port');
41 42
42 43 if (wsHost.length > 0 && wsPort.length > 0)
43 44 var centrifuge = new Centrifuge({
44 45 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
45 46 "project": metapanel.getAttribute('data-ws-project'),
46 47 "user": wsUser,
47 48 "timestamp": metapanel.getAttribute('data-last-update'),
48 49 "token": metapanel.getAttribute('data-ws-token'),
49 50 "debug": false
50 51 });
51 52
52 53 centrifuge.on('error', function(error_message) {
53 54 console.log("Error connecting to websocket server.");
54 55 return false;
55 56 });
56 57
57 58 centrifuge.on('connect', function() {
58 59 var channelName = 'thread:' + threadId;
59 60 centrifuge.subscribe(channelName, function(message) {
60 61 var postHtml = message.data['html'];
61 62 var isAdded = (message.data['diff_type'] === 'added');
62 63
63 64 if (postHtml) {
64 65 updatePost(postHtml, isAdded);
65 66 }
66 67 });
67 68
69 // For the case we closed the browser and missed some updates
70 getThreadDiff();
68 71 $('#autoupdate').text('[+]');
69 72 });
70 73
71 74 centrifuge.connect();
72 75
73 76 return true;
74 77 }
75 78
79 /**
80 * Get diff of the posts from the current thread timestamp.
81 * This is required if the browser was closed and some post updates were
82 * missed.
83 */
84 function getThreadDiff() {
85 var lastPost = threadPosts.last();
86 var threadId = threadPosts.first().attr('id');
87
88 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
89
90 lastUpdateTime = $('.metapanel').attr('data-last-update');
91
92 $.getJSON(diffUrl)
93 .success(function(data) {
94 var bottom = isPageBottom();
95 var addedPosts = data.added;
96
97 for (var i = 0; i < addedPosts.length; i++) {
98 var postText = addedPosts[i];
99 var post = $(postText);
100
101 updatePost(post, true)
102
103 lastPost = post;
104 }
105
106 var updatedPosts = data.updated;
107
108 for (var i = 0; i < updatedPosts.length; i++) {
109 var postText = updatedPosts[i];
110 var post = $(postText);
111
112 updatePost(post, false)
113 }
114
115 // TODO Process removed posts if any
116
117 lastUpdateTime = data.last_update;
118 })
119 }
120
121 /**
122 * Add or update the post on thml page.
123 */
76 124 function updatePost(postHtml, isAdded) {
77 125 // This needs to be set on start because the page is scrolled after posts
78 126 // are added or updated
79 127 var bottom = isPageBottom();
80 128
81 129 var post = $(postHtml);
82 130
83 131 var threadPosts = $('div.thread').children('.post');
84 132
85 133 var lastUpdate = '';
86 134
87 135 if (isAdded) {
88 136 var lastPost = threadPosts.last();
89 137
90 138 post.appendTo(lastPost.parent());
91 139
92 140 updateBumplimitProgress(1);
93 141 showNewPostsTitle(1);
94 142
95 143 lastUpdate = post.children('.post-info').first()
96 144 .children('.pub_time').first().text();
97 145
98 146 if (bottom) {
99 147 scrollToBottom();
100 148 }
101 149 } else {
102 150 var postId = post.attr('id');
103 151
104 152 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
105 153
106 154 oldPost.replaceWith(post);
107 155 }
108 156
109 157 processNewPost(post);
110 158 updateMetadataPanel(lastUpdate)
111 159 }
112 160
161 /**
162 * Initiate a blinking animation on a node to show it was updated.
163 */
113 164 function blink(node) {
114 165 var blinkCount = 2;
115 166
116 167 var nodeToAnimate = node;
117 168 for (var i = 0; i < blinkCount; i++) {
118 169 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
119 170 }
120 171 }
121 172
122 173 function isPageBottom() {
123 174 var scroll = $(window).scrollTop() / ($(document).height()
124 175 - $(window).height());
125 176
126 177 return scroll == 1
127 178 }
128 179
129 180 function initAutoupdate() {
130 181 return connectWebsocket();
131 182 }
132 183
133 184 function getReplyCount() {
134 185 return $('.thread').children('.post').length
135 186 }
136 187
137 188 function getImageCount() {
138 189 return $('.thread').find('img').length
139 190 }
140 191
192 /**
193 * Update post count, images count and last update time in the metadata
194 * panel.
195 */
141 196 function updateMetadataPanel(lastUpdate) {
142 197 var replyCountField = $('#reply-count');
143 198 var imageCountField = $('#image-count');
144 199
145 200 replyCountField.text(getReplyCount());
146 201 imageCountField.text(getImageCount());
147 202
148 203 if (lastUpdate !== '') {
149 204 var lastUpdateField = $('#last-update');
150 205 lastUpdateField.text(lastUpdate);
151 206 blink(lastUpdateField);
152 207 }
153 208
154 209 blink(replyCountField);
155 210 blink(imageCountField);
156 211 }
157 212
158 213 /**
159 214 * Update bumplimit progress bar
160 215 */
161 216 function updateBumplimitProgress(postDelta) {
162 217 var progressBar = $('#bumplimit_progress');
163 218 if (progressBar) {
164 219 var postsToLimitElement = $('#left_to_limit');
165 220
166 221 var oldPostsToLimit = parseInt(postsToLimitElement.text());
167 222 var postCount = getReplyCount();
168 223 var bumplimit = postCount - postDelta + oldPostsToLimit;
169 224
170 225 var newPostsToLimit = bumplimit - postCount;
171 226 if (newPostsToLimit <= 0) {
172 227 $('.bar-bg').remove();
173 228 $('.thread').children('.post').addClass('dead_post');
174 229 } else {
175 230 postsToLimitElement.text(newPostsToLimit);
176 231 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
177 232 }
178 233 }
179 234 }
180 235
181 var documentOriginalTitle = '';
182 236 /**
183 237 * Show 'new posts' text in the title if the document is not visible to a user
184 238 */
185 239 function showNewPostsTitle(newPostCount) {
186 240 if (document.hidden) {
187 241 if (documentOriginalTitle === '') {
188 242 documentOriginalTitle = document.title;
189 243 }
190 244 unreadPosts = unreadPosts + newPostCount;
191 245 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
192 246
193 247 document.addEventListener('visibilitychange', function() {
194 248 if (documentOriginalTitle !== '') {
195 249 document.title = documentOriginalTitle;
196 250 documentOriginalTitle = '';
197 251 unreadPosts = 0;
198 252 }
199 253
200 254 document.removeEventListener('visibilitychange', null);
201 255 });
202 256 }
203 257 }
204 258
205 259 /**
206 260 * Clear all entered values in the form fields
207 261 */
208 262 function resetForm(form) {
209 263 form.find('input:text, input:password, input:file, select, textarea').val('');
210 264 form.find('input:radio, input:checkbox')
211 265 .removeAttr('checked').removeAttr('selected');
212 266 $('.file_wrap').find('.file-thumb').remove();
213 267 }
214 268
215 269 /**
216 270 * When the form is posted, this method will be run as a callback
217 271 */
218 272 function updateOnPost(response, statusText, xhr, form) {
219 273 var json = $.parseJSON(response);
220 274 var status = json.status;
221 275
222 276 showAsErrors(form, '');
223 277
224 278 if (status === 'ok') {
225 279 resetForm(form);
226 280 } else {
227 281 var errors = json.errors;
228 282 for (var i = 0; i < errors.length; i++) {
229 283 var fieldErrors = errors[i];
230 284
231 285 var error = fieldErrors.errors;
232 286
233 287 showAsErrors(form, error);
234 288 }
235 289 }
236 290
237 291 scrollToBottom();
238 292 }
239 293
240 294 /**
241 295 * Show text in the errors row of the form.
242 296 * @param form
243 297 * @param text
244 298 */
245 299 function showAsErrors(form, text) {
246 300 form.children('.form-errors').remove();
247 301
248 302 if (text.length > 0) {
249 303 var errorList = $('<div class="form-errors">' + text
250 304 + '<div>');
251 305 errorList.appendTo(form);
252 306 }
253 307 }
254 308
255 309 /**
256 310 * Run js methods that are usually run on the document, on the new post
257 311 */
258 312 function processNewPost(post) {
259 313 addRefLinkPreview(post[0]);
260 314 highlightCode(post);
261 315 blink(post);
262 316 }
263 317
264 318 $(document).ready(function(){
265 319 if ('WebSocket' in window) {
266 320 if (initAutoupdate()) {
267 321 // Post form data over AJAX
268 322 var threadId = $('div.thread').children('.post').first().attr('id');
269 323
270 324 var form = $('#form');
271 325
272 326 var options = {
273 327 beforeSubmit: function(arr, $form, options) {
274 328 showAsErrors($('form'), gettext('Sending message...'));
275 329 },
276 330 success: updateOnPost,
277 331 url: '/api/add_post/' + threadId + '/'
278 332 };
279 333
280 334 form.ajaxForm(options);
281 335
282 336 resetForm(form);
283 337 }
284 338 }
285 339 });
@@ -1,159 +1,157 b''
1 1 from django.core.urlresolvers import reverse
2 2 from django.db import transaction
3 3 from django.http import Http404
4 4 from django.shortcuts import get_object_or_404, render, redirect
5 from django.views.decorators.cache import never_cache, cache_control
6 5 from django.views.generic.edit import FormMixin
7 6
8 7 from boards import utils, settings
9 8 from boards.forms import PostForm, PlainErrorList
10 9 from boards.models import Post, Ban
11 10 from boards.views.banned import BannedView
12 11 from boards.views.base import BaseBoardView, CONTEXT_FORM
13 12 from boards.views.posting_mixin import PostMixin
14 13 import neboard
15 14
16 15 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
17 16 TEMPLATE_NORMAL = 'boards/thread.html'
18 17
19 18 CONTEXT_POSTS = 'posts'
20 19 CONTEXT_OP = 'opening_post'
21 20 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
22 21 CONTEXT_POSTS_LEFT = 'posts_left'
23 22 CONTEXT_LASTUPDATE = "last_update"
24 23 CONTEXT_MAX_REPLIES = 'max_replies'
25 24 CONTEXT_THREAD = 'thread'
26 25 CONTEXT_BUMPABLE = 'bumpable'
27 26 CONTEXT_WS_TOKEN = 'ws_token'
28 27 CONTEXT_WS_PROJECT = 'ws_project'
29 28 CONTEXT_WS_HOST = 'ws_host'
30 29 CONTEXT_WS_PORT = 'ws_port'
31 30
32 31 FORM_TITLE = 'title'
33 32 FORM_TEXT = 'text'
34 33 FORM_IMAGE = 'image'
35 34
36 35 MODE_GALLERY = 'gallery'
37 36 MODE_NORMAL = 'normal'
38 37
39 38
40 39 class ThreadView(BaseBoardView, PostMixin, FormMixin):
41 40
42 @cache_control(no_cache=True)
43 41 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
44 42 try:
45 43 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
46 44 except IndexError:
47 45 raise Http404
48 46
49 47 # If this is not OP, don't show it as it is
50 48 if not opening_post or not opening_post.is_opening():
51 49 raise Http404
52 50
53 51 if not form:
54 52 form = PostForm(error_class=PlainErrorList)
55 53
56 54 thread_to_show = opening_post.get_thread()
57 55
58 56 context = self.get_context_data(request=request)
59 57
60 58 context[CONTEXT_FORM] = form
61 59 context[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch(
62 60 thread_to_show.last_edit_time))
63 61 context[CONTEXT_THREAD] = thread_to_show
64 62 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
65 63
66 64 if settings.WEBSOCKETS_ENABLED:
67 65 context[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
68 66 timestamp=context[CONTEXT_LASTUPDATE])
69 67 context[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
70 68 context[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
71 69 context[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
72 70
73 71 # TODO Move this to subclasses: NormalThreadView, GalleryThreadView etc
74 72 if MODE_NORMAL == mode:
75 73 bumpable = thread_to_show.can_bump()
76 74 context[CONTEXT_BUMPABLE] = bumpable
77 75 if bumpable:
78 76 left_posts = settings.MAX_POSTS_PER_THREAD \
79 77 - thread_to_show.get_reply_count()
80 78 context[CONTEXT_POSTS_LEFT] = left_posts
81 79 context[CONTEXT_BUMPLIMIT_PRG] = str(
82 80 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
83 81
84 82 context[CONTEXT_OP] = opening_post
85 83
86 84 document = TEMPLATE_NORMAL
87 85 elif MODE_GALLERY == mode:
88 86 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
89 87 view_fields_only=True)
90 88
91 89 document = TEMPLATE_GALLERY
92 90 else:
93 91 raise Http404
94 92
95 93 # TODO Use dict here
96 94 return render(request, document, context_instance=context)
97 95
98 96 def post(self, request, post_id, mode=MODE_NORMAL):
99 97 opening_post = get_object_or_404(Post, id=post_id)
100 98
101 99 # If this is not OP, don't show it as it is
102 100 if not opening_post.is_opening():
103 101 raise Http404
104 102
105 103 if not opening_post.get_thread().archived:
106 104 form = PostForm(request.POST, request.FILES,
107 105 error_class=PlainErrorList)
108 106 form.session = request.session
109 107
110 108 if form.is_valid():
111 109 return self.new_post(request, form, opening_post)
112 110 if form.need_to_ban:
113 111 # Ban user because he is suspected to be a bot
114 112 self._ban_current_user(request)
115 113
116 114 return self.get(request, post_id, mode, form)
117 115
118 116 @transaction.atomic
119 117 def new_post(self, request, form, opening_post=None, html_response=True):
120 118 """Add a new post (in thread or as a reply)."""
121 119
122 120 ip = utils.get_client_ip(request)
123 121 is_banned = Ban.objects.filter(ip=ip).exists()
124 122
125 123 if is_banned:
126 124 if html_response:
127 125 return redirect(BannedView().as_view())
128 126 else:
129 127 return None
130 128
131 129 data = form.cleaned_data
132 130
133 131 title = data[FORM_TITLE]
134 132 text = data[FORM_TEXT]
135 133
136 134 text = self._remove_invalid_links(text)
137 135
138 136 if FORM_IMAGE in list(data.keys()):
139 137 image = data[FORM_IMAGE]
140 138 else:
141 139 image = None
142 140
143 141 tags = []
144 142
145 143 post_thread = opening_post.get_thread()
146 144
147 145 post = Post.objects.create_post(title=title, text=text, image=image,
148 146 thread=post_thread, ip=ip, tags=tags)
149 147 post.send_to_websocket(request)
150 148
151 149 thread_to_show = (opening_post.id if opening_post else post.id)
152 150
153 151 if html_response:
154 152 if opening_post:
155 153 return redirect(
156 154 reverse('thread', kwargs={'post_id': thread_to_show})
157 155 + '#' + str(post.id))
158 156 else:
159 157 return post
General Comments 0
You need to be logged in to leave comments. Login now