diff --git a/.gitignore b/.gitignore index 9785c98..0b3ff61 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,4 @@ dmypy.json node_modules /static /core/static/dist +.env/ diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..dbb3228 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +* zodman +* jonathan-s diff --git a/app/urls.py b/app/urls.py index fa8bc0f..c89a875 100644 --- a/app/urls.py +++ b/app/urls.py @@ -5,6 +5,7 @@ import core.views.example import core.views.book_search +import core.views.chat urlpatterns = [ @@ -13,4 +14,5 @@ path('book-search/', core.views.book_search.book_search, name='book_search'), path('example/', core.views.example.example, name='example'), + path('chat/', core.views.chat.chat, name='chat'), ] + staticfiles_urlpatterns() diff --git a/core/javascript/controllers/chat_controller.js b/core/javascript/controllers/chat_controller.js new file mode 100644 index 0000000..7f080f3 --- /dev/null +++ b/core/javascript/controllers/chat_controller.js @@ -0,0 +1,52 @@ +import Rails from '@rails/ujs' +import { debounce } from 'lodash-es' +import ApplicationController from './application_controller' + +let lastMessageId +const reload = controller => { + controller.stimulate('ChatReflex#reload') +} +const debouncedReload = debounce(reload, 100) + +export default class extends ApplicationController { + static get targets () { + return ['list', 'input'] + } + + connect () { + super.connect() + this.scroll(100) + } + + post (event) { + console.log("post"); + Rails.stopEverything(event) + lastMessageId = Math.random() + this.stimulate( + 'ChatReflex#post', + this.element.dataset.color, + this.inputTarget.value, + lastMessageId + ) + } + + afterPost () { + this.inputTarget.value = '' + this.inputTarget.focus() + this.scroll(1) + } + + scroll (delay = 10) { + const lists = document.querySelectorAll('[data-target="chat.list"]') + setTimeout(() => { + lists.forEach(e => (e.scrollTop = e.scrollHeight)) + }, delay) + } + + reload (event) { + const { messageId } = event.detail + if (messageId === lastMessageId) return + debouncedReload(this) + } +} + diff --git a/core/javascript/example.js b/core/javascript/example.js index 1c72462..6d732b2 100644 --- a/core/javascript/example.js +++ b/core/javascript/example.js @@ -1,20 +1,28 @@ import { Application } from 'stimulus' import StimulusReflex from 'stimulus_reflex' import WebsocketConsumer from 'sockpuppet-js' - +import CableReady from 'cable_ready' import debounced from 'debounced' import BookSearchController from './controllers/book_search_controller' import ExampleController from './controllers/example_controller' +import ChatController from './controllers/chat_controller' debounced.initialize() -//import TurboLinks from 'turbolinks' +import TurboLinks from 'turbolinks' -//TurboLinks.start() +TurboLinks.start() const application = Application.start() const ssl = location.protocol !== 'https:' ? '' : 's'; const consumer = new WebsocketConsumer(`ws${ssl}://${location.hostname}:${location.port}/ws/sockpuppet-sync`) +consumer.subscriptions.create('ChatChannel', { + received (data) { + if (data.cableReady) CableReady.perform(data.operations) + } +}) + application.register("example", ExampleController) application.register("book-search", BookSearchController) +application.register("chat", ChatController) StimulusReflex.initialize(application, { consumer, debug: true }) diff --git a/core/reflexes/book_search_reflex.py b/core/reflexes/book_search_reflex.py index 0b448dd..22007a0 100644 --- a/core/reflexes/book_search_reflex.py +++ b/core/reflexes/book_search_reflex.py @@ -3,9 +3,9 @@ class BookSearchReflex(Reflex): - def perform(self, query=""): - resp = requests.get('http://openlibrary.org/search.json', params={'q':query}) + def perform(self, query=''): + resp = requests.get('http://openlibrary.org/search.json', params={'q': query}) resp.raise_for_status() books = resp.json() - self.books = books.get("docs", []) - self.count = books["num_found"] + self.books = books.get('docs', []) + self.count = books['num_found'] diff --git a/core/reflexes/chat_reflex.py b/core/reflexes/chat_reflex.py new file mode 100644 index 0000000..2b143e6 --- /dev/null +++ b/core/reflexes/chat_reflex.py @@ -0,0 +1,21 @@ +from sockpuppet.reflex import Reflex +from sockpuppet.channel import Channel +from django.core.cache import cache +from django.utils import timezone + + +class ChatReflex(Reflex): + def post(self, color, message, message_id): + chats = cache.get("chats", []) + chats.append({ + 'message': message, + 'message_id': message_id, + 'created_at': timezone.now() + }) + cache.set("chats", chats) + channel = Channel("chat") + channel.dispatch_event({ + 'name': 'chats:added', + 'detail': {'messagre_id': message_id} + }) + channel.broadcast() diff --git a/core/templates/_chat_demo.html b/core/templates/_chat_demo.html new file mode 100644 index 0000000..2cebf93 --- /dev/null +++ b/core/templates/_chat_demo.html @@ -0,0 +1,21 @@ + +
+ + {% for chat in chats %} + + {% endfor %} + +
+ + +
+ Message storage on LocMemCache +
+
+
diff --git a/core/templates/base.html b/core/templates/base.html index 793d5f4..fffaa7e 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -24,7 +24,7 @@ src="https://i.imgur.com/FX0KFM7m.jpg" style="filter: grayscale(0.9) contrast(1.2); height: 4rem;" alt="" />

- Sockpuppet + Sockpuppet Expo


@@ -34,21 +34,23 @@

-

{% block subtitle %}

Django Sockpuppet

{% endblock subtitle %}

+

{% block subtitle %}Django Sockpuppet{% endblock subtitle %}

{% block main %} {% endblock %} diff --git a/core/templates/index.html b/core/templates/index.html index ce9464a..c575439 100644 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -1,4 +1,11 @@ {% extends "base.html" %} {% block main %} +

+It is an exciting new way to build reactive, modern real-time apps. +It eliminates the hassle of maintaining state on the client. +It's a new way of thinking... and it works with technologies that Django developers already use, like server rendered HTML, Russian Doll caching, Stimulus and Turbolinks. +
+ The demos on this site are an attempt to teach others how to build reactive applications with django-sockpuppet. +

{% endblock main %} diff --git a/core/views/book_search.py b/core/views/book_search.py index e7ba2c3..f61dd85 100644 --- a/core/views/book_search.py +++ b/core/views/book_search.py @@ -1,15 +1,9 @@ from django.views.generic.base import TemplateView -from .mixins import MixinBase +from .mixins import BookSearchMixin -class BookSearch(MixinBase, TemplateView): +class BookSearch(BookSearchMixin, TemplateView): demo_template = "_book_search_demo.html" subtitle = 'Search Book' - files = ( - ('core/reflexes/book_search_reflex.py', 'python', 'python3'), - ('core/views/book_search.py', 'python', 'python3'), - ('core/javascript/controllers/book_search_controller.js', 'javascript', 'javascript'), - ('core/templates/_book_search_demo.html', 'html', 'htmldjango'), - ) book_search = BookSearch.as_view() diff --git a/core/views/chat.py b/core/views/chat.py new file mode 100644 index 0000000..1d5ab02 --- /dev/null +++ b/core/views/chat.py @@ -0,0 +1,16 @@ +from django.views.generic.base import TemplateView +from django.core.cache import cache +from .mixins import ChatMixin + +class ChatView(ChatMixin, TemplateView): + demo_template = '_chat_demo.html' + subtitle = 'Chat' + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context['chats'] = cache.get("chats", []) + return context + +chat = ChatView.as_view() + + diff --git a/core/views/example.py b/core/views/example.py index 6825b3e..4089b45 100644 --- a/core/views/example.py +++ b/core/views/example.py @@ -1,15 +1,9 @@ from django.views.generic.base import TemplateView -from .mixins import MixinBase +from .mixins import ExampleMixin -class ExampleView(MixinBase, TemplateView): +class ExampleView(ExampleMixin, TemplateView): demo_template = '_example_demo.html' subtitle = 'Increment' - files = ( - ('core/views/example.py', 'python', 'python3'), - ('core/reflexes/example_reflex.py', 'python', 'python3'), - ('core/javascript/controllers/example_controller.js', 'javascript', 'javascript'), - ('core/templates/_example_demo.html', 'html', 'htmldjango'), - ) def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) diff --git a/core/views/mixins.py b/core/views/mixins.py index 17eb9a1..89e4c84 100644 --- a/core/views/mixins.py +++ b/core/views/mixins.py @@ -6,27 +6,56 @@ class MixinBase: - template_name="demo.html" + template_name = "demo.html" demo_template = None subtitle = None def get_files(self): files = defaultdict(list) path_ = lambda x: open(os.path.join(BASE_PATH, x)).read() - for filename, filetype, pygment_type in self.files: + for filename, filetype, pygment_type in self.files: filesrc = path_(filename) - files[filetype].append({ - 'src': filesrc, - 'pygment_type': pygment_type, - 'filename': filename, - 'loc': len(filesrc.split('\n')) - }) + files[filetype].append( + { + "src": filesrc, + "pygment_type": pygment_type, + "filename": filename, + "loc": len(filesrc.split("\n")), + } + ) return dict(files) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['files'] = self.get_files() - context['demo_template'] = self.demo_template - context['subtitle'] = self.subtitle + context["files"] = self.get_files() + context["demo_template"] = self.demo_template + context["subtitle"] = self.subtitle return context + +class BookSearchMixin(MixinBase): + files = ( + ("core/reflexes/book_search_reflex.py", "python", "python3"), + ("core/views/book_search.py", "python", "python3"), + ("core/javascript/controllers/book_search_controller.js", "javascript", + "javascript",), + ("core/templates/_book_search_demo.html", "html", "htmldjango"), + ) + + +class ExampleMixin(MixinBase): + files = ( + ('core/views/example.py', 'python', 'python3'), + ('core/reflexes/example_reflex.py', 'python', 'python3'), + ('core/javascript/controllers/example_controller.js', 'javascript', 'javascript'), + ('core/templates/_example_demo.html', 'html', 'htmldjango'), + ) + +class ChatMixin(MixinBase): + files = ( + ('core/views/chat.py', 'python', 'python3'), + ('core/reflexes/chat_reflex.py', 'python', 'python3'), + ('core/javascript/controllers/chat_controller.js', 'javascript', 'javascript'), + ('core/templates/_chat_demo.html', 'html', 'htmldjango'), + ) + diff --git a/package.json b/package.json index 953f1d1..1192c24 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,10 @@ "stimulus_reflex": "^3.4.0-pre5", "webpack": "^5.6.0", "webpack-cli": "^4.2.0" + }, + "dependencies": { + "@rails/ujs": "^6.0.3-4", + "lodash-es": "^4.17.15", + "turbolinks": "^5.2.0" } } diff --git a/yarn.lock b/yarn.lock index 016d5dd..6f59fe0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.3.tgz#722b4b639936129307ddbab3a390f6bcacf3e7bc" integrity sha512-I01hgqxxnOgOtJTGlq0ZsGJYiTEEiSGVEGQn3vimZSqEP1HqzyFNbzGTq14Xdyeow2yGJjygjoFF1pmtE+SQaw== +"@rails/ujs@^6.0.3-4": + version "6.0.3-4" + resolved "https://nexus.gcds.coke.com/repository/npm-group/@rails/ujs/-/ujs-6.0.3-4.tgz#8dafc84178080f9c4f21076953ea1d0dc0bfe0fc" + integrity sha512-pNEEndJYNMCYEZG79MkoMc40AYKBfm0md8pawJ/SUu/1aIhToJcKu+9hHT/7WMLudsakOgC/C8KKFuZOs4QTgw== + "@stimulus/core@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@stimulus/core/-/core-1.1.1.tgz#42b0cfe5b73ca492f41de64b77a03980bae92c82" @@ -639,6 +644,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash-es@^4.17.15: + version "4.17.15" + resolved "https://nexus.gcds.coke.com/repository/npm-group/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" + integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== + lodash@^4.17.15: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" @@ -976,6 +986,11 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +turbolinks@^5.2.0: + version "5.2.0" + resolved "https://nexus.gcds.coke.com/repository/npm-group/turbolinks/-/turbolinks-5.2.0.tgz#e6877a55ea5c1cb3bb225f0a4ae303d6d32ff77c" + integrity sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw== + typical@^5.0.0, typical@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"