Quantcast
Channel: Discourse Meta - Latest topics
Viewing all 60599 articles
Browse latest View live

Adding an interface adjacent to a conversation (with a plugin)

$
0
0

@paulm wrote:

I'd like to create a plugin that integrates a separate user interface with a topic. This is what I'm thinking:

So the topic would be as normal, except each post can have a separate, but linked, interface. If a post has a linked interface, it needs to be displayed in line with the post, and the following posts would be pushed down.

In my use case, the interface would allow the user to produce simple graphs, to complement the text in their post. The data would be stored with the post (similar to the polls plugin?). In fact, the whole thing would be similar to the polls plugin, if the poll were off to the side rather than inline in the post.

Is such a thing possible in a Discourse plugin?
I guess the main issue would be extending the UI?

Any questions or thoughts are appreciated!

Posts: 1

Participants: 1

Read full topic


Allow moderators to create groups

$
0
0

@jomaxro wrote:

Yesterday, @codinghorror announced a new feature to try and curb behavior that can cause performance issues (see Using private / personal messages as long term chat). After a bit of discussion, @sam and I had a conversation about PMs vs private categories, and I learned that other than visibility, they are not very different. Other than their visibility, particularly with group PMs now being first class citizens, the biggest difference is who can create them. At the moment, with default settings anyone can create a PM to a xx people, but only admins can create groups, and thus private categories.

Allow moderators to create groups

The main feature request is to allow moderators to create groups, in a similar manner to how they create categories.

Necessary changes

  1. New site setting allow moderators to create groups
  2. Allow moderators to see the groups page in the Admin interface
  3. Allow moderators to create groups, assign group owners, members, and change all other group settings

Potential Issues

How to continue to allow Admins to create groups that are not visible to Moderators?

Proposed Solution
It is currently possible for a moderator to create a new category they cannot see after creation (by assigning it to be viewable only to Admins. Whether this is a bug or not is an issue for another time. I would propose a similar solution for group creation. Create a single check box (ideally viewable only by Admins) that says Do not make group visible to moderators. This option would only appear if the allow moderators to create groups is enabled, and would allow for admin only groups to still be created. If this box is checked, then these groups do not appear for moderators in the groups interface, just like categories are not shown to those users without proper permissions on the /categories page.

Posts: 2

Participants: 2

Read full topic

Summary outcome of the debate on wysiwyg?

$
0
0

@Rob_Nicholson wrote:

Continuing the discussion from Who would prefer a standard wysiwyg to markdown?:

The above discussion was pretty busy and the thread is now closed with some comment about a proof of concept. To save me having to read all 77 posts, could somebody kindly give me a summary of the next steps? Don't want to kick start the discussion again - just a summary of where Discourse is going on this subject and maybe a time frame?

If there is a vote, then a BIG vote for a wysiwyg option. As a programmer and reader of Coding Horror, I'm well aware of the back story, positives etc but usability and familiarity for 180,000 non-technical www.camra.org.uk members is very high on our requirements.

Posts: 9

Participants: 3

Read full topic

Private message composition URLs don't work with SSO

$
0
0

@toolness wrote:

Hello! Discourse is awesome but I seem to be encountering an odd bug related to Discourse SSO and the Compose a new pre-filled private message via URL functionality.

Here's what's going on.

  1. A user is logged into my SSO site at https://example.org. They have not yet visited my Discourse site at https://discourse.example.org, so they don't have a session established there yet.
  2. While on my SSO site, the user clicks on a link that takes them to https://discourse.example.org/new-message?username=foo. (Assume that foo is a valid user on my Discourse install.)
  3. My Discourse site notices that the user isn't logged-in and redirects them to my SSO site, which immediately sends them back to my Discourse site.
  4. For a fraction of a second, the URL in the user's address bar appears to be https://discourse.example.org/new-message, but it then changes to https://discourse.example.org/.
  5. A modal with the text "Sorry, an error has occurred" appears on the Discourse site.

This bug only seems to occur when the user clicks on a private message composition link and isn't yet logged in to Discourse. Everything works fine if the user already has a Discourse session.

Everything also works fine if the user isn't yet logged into Discourse and clicks a non-PM-composition link. For example, if the user clicks on a link to a specific topic, Discourse navigates to that topic after the SSO handshake without any problems. It's only with PM composition links that Discourse appears to "forget" that it was supposed to open a PM composition link to foo after the SSO handshake.

Does this make sense? Is anyone else experiencing the same problem? If this is actually a bug, I'm willing to investigate the issue and submit a PR to fix it.

Posts: 1

Participants: 1

Read full topic

Youtube fullscreen button transfers you to another post in topic

$
0
0

@Sander78 wrote:

Have tried to repo this on Try, but could not. This post on the Meteor forum has a Youtube video. If you play it and press the full screen button, you get full screen for a second and then you're transfered to a completely different post in the topic...

Screencast:

Posts: 2

Participants: 2

Read full topic

Badge system deactivated, but badges still appear on some profiles

$
0
0

@commonpawn wrote:

On our discourse installation we disabled the badge system in settings. However, it is still possible to see some user's badges on the summary pages as well as it is possible to access the badge overview pages.

Not sure what and why - but it seems like users who were around before we disabled the badge system still have their badges visible. Not sure if this is a bug or intentional, but we would like them to be hidden as we're not using the system so it is rather confusing.

Posts: 3

Participants: 3

Read full topic

Left clicking link to /posts.rss does not work

$
0
0

@ryantm wrote:

Left clicking this link does not work for me (displays "Oops! That page doesn’t exist or is private."), but middle clicking it or copying the URL works fine:

link to posts rss

I am using Chrome Version 49.0.2623.87 m on Windows 10.

Posts: 10

Participants: 4

Read full topic

On the Necessity of Up/Down Vote-Based Reputation Systems on Boards Involving Subjective Discussion

$
0
0

@Alex1974 wrote:

I am using Discourse for a moderately-sized political discussion board.

The software is very good, and meets all of my needs except one: there is no up/down vote-based reputation system.

What I would like to see is a very simple system, where up/down votes are 1-to-1. The poster starts out with zero rep, and each vote by the community adds or subtracts a single point (better would possibly be that a poster with more rep points' vote would be worth more than a poster with low rep points, but that wouldn't be necessary). The reputation level would then be visible (somewhere) on each post. It would b very similar to the system Reddit uses.

Beyond this - and I know some people won't like this idea - I would, if I could, implement a feature where if a person receives a certain amount of down-votes within a certain time period, they are automatically suspended for a temporary period. This would serve to fully democratize the moderation process, which is the goal of my proposed feature.

I have read all of the existing threads on this board discussing the concept of up/down voting and reputation points, and understand the position of the developers. Because this discussion has been largely philosophical in nature, rather than practical, I will address the philosophical along with the practical elements which create a situation where disapproving judgements by the community are needed to discourage negative forum behavior.

In the previous threads, the development team has presented an argument very similar to the arguments for purely positive-reinforcement-based parenting. This is an interesting topic which I've read quite a bit about. Though I find the concept somewhat utopian, many experts genuinely believe in it.

However, while this method may or may not work with children at home, what has been proven is that in a school or daycare setting, negative-reinforcement is necessary in order to maintain order. Even universities, obviously, have systems of negative-reinforcement to deal with anti-social or otherwise problematic/disruptive behavior. And what I deal with on my forum is a similar situation to that of a school, where personalities are prone to conflicting with one another and a system is needed to dissuade people from engaging in problematic behavior.

I understand that on support forums, such as this one or the Ubuntu board, there isn't a need for negative reinforcement, as the communities have very specific purposes, and personalities and emotions don't really enter into the scheme of interpersonal interaction. A positive-only reputation system, based on the existing badge system, works perfect in this environment, as the quality of a poster is based on their knowledge of the subject matter, rather than something more personal/nuanced.

However, on a large board where many emotional (and even personal) issues are discussed, I have found that chaos ensues if there is not a mechanism of negative reinforcement to discourage negative forum behavior.

Presently, the only means I have of using negative-reinforcement are manual and unilateral: I can temporarily suspend accounts or ban people outright. This strikes me as primitive, and often gratuitous, as well as non-democratic/authoritarian.

Within the context of political discussion, there is significantly more room for negative posting because you are dealing with the subjective, rather than the objective (the latter being what you are dealing with on a support forum, obviously). The emotional nature of subjective discussions leads to all different types of negative behavior, including insults/name-calling, the purposeful (or not) spreading of wrong information and stating positions on subjects that one knows nothing about.

A reputation system would serve two main purposes:

  1. To let the poster him/herself know that people do not like their posts, which will in some (hopefully most) cases cause them to reflect on what they are posting. Without this feedback, they may post indefinitely without being aware of the fact that people dislike their posts. Positive reinforcement can't work here, because the poster who doesn't receive many likes (or a badge for all the likes they've received) will likely be unaware they haven't received this positive feedback.

  2. To let other posters know that the poster in question is considered by the community to be a low-quality poster, which will make them much less likely to engage in debate/discussion with someone who the community considers to not be worth the time/effort. This is certainly more of an issue the larger the board becomes, as a poster is less likely to be familiar with all of the other posters on the board.

The importance of this to a large community where emotional discussion takes place cannot be overstated.

Beyond this, the current system of no reputation makes certain that everyone in a discussion is on the same footing at all times. I can see how in a Marxist/egalitarian sense that could be considered positive, however, on a forum where emotionally-triggering issues are discussed, people will often create an account solely for the purpose of arguing with others. Presently, there is very little ability to distinguish between quality posters who are integrated members of the community and people who have showed up simply to be contrarian.

Again, the option of using banning as a means to deal with posters who disagree with others is very primitive. It also results, in my experience, in a slippery slope, where anyone can potentially be banned for anything, as there is no way to scientifically define "flaming" or "trolling." When people see others being banned for contrarian posting, they then fear making any statement disagreeing with another poster, and many will ultimately abandon the community due to a feeling of being arbitrarily restricted.

As a totally separate point (which is not especially important to me, but is worth mentioning), a reputation system encourages posters to work hard to provide good content. With the present system of badges, that incentive is much less prominent.

I am not myself a coder, but I would think that the system I am arguing for would be relatively simple to implement. A poster's total "likes" are already calculated, so I would imagine you could just convert that system to an up/down system, where likes could be removed, then insert that number underneath posters' usernames.

Finally, to wrap-up my pitch here, I will say that there is an ongoing battle between Discourse and Nodebb. Overall, it appears that Discourse is much better software. The problem I see people having, and the biggest danger to Discourse in this most important time of competition, is the lack of customizations leading to a less satisfying user experience. The up/down vote and rep system goes a long way toward enhancing the user experience.

More importantly, of course, is the ability to mitigate conflict on larger boards dealing with non-objective subject matter.

Looking at the directory, I see that the main use of this software presently is for support forums and closely-knit niche discussion boards, where conflict between users is generally virtually non-existent and thus there is little need for the democratization of quality control.

Adding the option of up/down votes would open up a whole new range of usability to Discourse, both on the level of the user experience and on the practical level of managing the community in a democratic fashion.

Clearly, due to the philosophical leanings of the development team, the positive-reinforcement only model should remain the default. However, options are always a good thing, and I am certain that I am not the only board admin who understands that the natural climate of my community requires negative-reinforcement in order to mitigate conflict between users and create an environment where quality posting is encouraged.

I'm very interested to get other people's thoughts on this.

Posts: 11

Participants: 4

Read full topic


Automatic sign up + login from javascript client

$
0
0

@kofifus wrote:

Newbe question here, I searched the forum but could not find a clear answer.

My scenario is simple and I would imagine reasonably common.

I have a server-less javascript client application (working with a no-sql cloud db). So all my code runs in javascript on the user's browser client.

Users go through a registration process on my site. At the end of that process the javascript browser client holds the email/password the user registered with (after passing email verification etc). At this point I want to automatically register the user to my linked discourse domain. I want to do that from the client using javascript without any more prompts to the user.

After registration is done and the user logs in with the credentials he registered, The javascript browser client now holds the email/password used to log in. I now want my help page to provide a link that will directly logs the user to my discourse domain so he can log an issue etc.

Is this possible ?

If yes is there any existing code/directions for this ?

Thank you!

Posts: 2

Participants: 2

Read full topic

Improving our unsubscribe functionality

$
0
0

@sam wrote:

Over the last week I have been thinking a lot about our "mail problem"

The problem is that on certain very active instances of Discourse tons of mails can be generated, which inevitably leads some people to click the "spam" button, and hurt deliverability of all mails from that site.

It is easy to blame this on specific features:

  • We allow people to easily enable mailing list mode which can lead to email floods on mega active sites

  • Watching a very active category is death by a thousand papercuts, first week is easy, but 2 months in you have 500 watched topics and are being flooded with no easy way out.

  • People can enable "email me even when I am online" or participate in a heavily heated private message and flood inboxes

I am working on adding a global rate limit for email so at least there is a limit to the amount of emails we send a user. I am also working on a few features that discourage some of the more common reasons this happens.

However, none of my work is going to attack the root cause.

It is simply too hard to unsubscribe

I saw this tweet from Scott Hanselman today:

The key problem here is I don't want to spend 1 minute, I want to simply do it all, unconditionally in 2 clicks.

Facebook has faced this issue forever, at the bottom of every email it has the text:

This message was sent to my@email.com . If you don't want to receive these emails from Facebook in the future, please unsubscribe.
Facebook, Inc., Attention: Community Support, Menlo Park, CA 94025

The unsubscribe link they generate is usable on a logged out browser

Compare that to our notifications that have:

To stop receiving notifications for this particular topic, click here. To unsubscribe from these emails, change your user preferences

If I click here as a logged out user, I get:

If I click prefs I get:

Given these 2 options, some users will simple go for this button:

Users don't want to deal with logging on to yet another site. They just want to stop getting the emails. Very often you decide to reach for the "unsubscribe" button a year later, the first 20 digest emails you simply delete, its that 21st one that makes you pause and think, I might as well unsubscribe.

Given that users these days are generally, multi device users (have phones, laptops and desktops) odds of being logged out are quite high. Odds of being on phone where it is a royal pain to log in are also high.

My proposal

Simplify the unsubscribe text:

Instead of:

To stop receiving notifications for this particular topic, click here. To unsubscribe from these emails, change your user preferences

a simple:

If you don't want to receive these emails from Site Name in the future, please unsubscribe.

Have the unsubscribe link be unique per email

example: https://sitename.com/unsubscribe?id=SkfSuhei72klf7klffhj

Have emails parser be careful to anonymise these links.

Have these links have a shelf life (only work for say 1 month configurable by site setting)

Simplify the unsubscribe page

When you click on the unsubscribe link present with something like this (depending on context)

Opt out of email notifications:

[x] Stop watching "This awesome topic"
[ ] Stop watching "Bugs" category and 72 topics in the "Bugs" category
[ ] Stop sending me emails from Sitename.com

For mailing list enabled

Opt out of email notifications:

[x] Disable mailing list mode
[ ] Stop sending me emails from Sitename.com

For digest

Opt out of email notifications:

[x] Stop sending me weekly digests
[ ] Stop sending me emails from Sitename.com

This makes it trivial to "get off the train" and eliminates the reason people reach for the spam button. There is too much friction now.

Posts: 11

Participants: 6

Read full topic

Special URL Separate from Homepage

$
0
0

@Safa_Faheem wrote:

Hi guys,

I'm back again with another question. I wanted to know if it is possible to have a "special URL" separate from the main public URL that would still take the user to our website but it would skip the login and go straight to the forums.

So, right now, we have our main website URL that I'll call X. If I go to X, I see the home page and a "log in" button. When I enter my credentials and successfully login, I am taken to the Discourse forum (similar to this one) for our website.

How do I have a "sub" website URL, maybe called like X2, that skips the home page, skips the login, and directly takes the user to the forum? They probably shouldn't have the ability to post but should have the ability to view and browse the threads.

Is this possible? Please let me know!

Appreciate everyone's help as always.

Safa

Posts: 11

Participants: 4

Read full topic

Misaligned sub-category swatch in email

$
0
0

@gdpelican wrote:

This is a wee thing, but I came across it a few times so I thought I'd report it.

When receiving a digest (or soon-to-exist mailing list) summary email, the category swatches for topics in subcategories are misaligned:

I can repro on mailcatcher locally, or with gmail receiving emails from meta.

Posts: 4

Participants: 2

Read full topic

Registraton attempts

$
0
0

@charleswalter wrote:

Has anyone ever tried looking at how many registration attempts they have had on their Discourse site? Curious to see what our conversion is from registration attempts to completed registrations.

If we had a Google Analytics event associated with the registration / login popup, that might help evaluate for future reference.

Posts: 5

Participants: 5

Read full topic

Topic Visitors Counter Plugin

$
0
0

@saiqulhaq wrote:

Hi
I've made a simple plugin to display how many visitors are visiting a topic.

https://github.com/saiqulhaq/tvc

This plugin calculates visitors based on session id, so it will not increment the total viewer data if there is multiple browser tabs opening same topic.
It will track user when entering or leaving to another topic, but it can't update the data if user close the browser tab. However it's not a big issue, I already have plan for that, I can set expire time for each tracked session and use heartbeat system to check that user still viewing the topic.

I will start to add that functionality and some test after GSOC student apply deadline, so the background of why I create this plugin is because I need more familiarize my self with Discourse.
Few days ago I sent a GSOC Proposal for 'Webhook for Discourse events', but I still have no feedback from mentors, and I can't find who is the mentor, so please let me know who is the mentor so I can contact with him.
Based on this groundwork, I want to propose 'See who's writing' GSOC idea.

I know it's not useful plugin, but I will very appreciate every feedbacks for this plugin.

Posts: 5

Participants: 3

Read full topic

Making Facebook app public for FB login

$
0
0

@charleswalter wrote:

I finally discovered today after 6+ months live with FB login that I had never made the FB HelloForos app public :(. I could have sworn we had FB logins. I had previously tested with my own personal account, but since I'm an admin on the app, I wasn't shown an error.

Just thought I'd share and I hope no one else makes the same mistake.

Posts: 3

Participants: 2

Read full topic


Daily limits for outgoing mails per user

$
0
0

@sam wrote:

I just checked in a feature that puts a cap on the number of emails we can send a user per day.

Out of the box max_emails_per_day_per_user of 100 emails per day is applied. You may bypass this limit by setting it to 0 or make it bigger.

When a user hits the limit we will display

WARNING: you reached the limit of 100 daily emails. Further emails notifications will be suppressed.

This is displayed at the bottom of the 100th emails.

Cap does not apply to "forgot password" (where a different cap applies) - it applies to email notifications and mailing list notifications.

May your sendgrid bills be cheaper...

Posts: 14

Participants: 5

Read full topic

Login with Facebook/google accounts using api for a mobile APP

$
0
0

@zinda_xyz wrote:

It would also be incredible to allow new users to register or login using Facebook/google accounts.

Is this possible?

i can login with username and password using api but how can i login with Facebook/google accounts in api

Posts: 1

Participants: 1

Read full topic

Uploads and profile functions broken, gives ruby error

$
0
0

@figlet wrote:

For many users, uploads are broken. For some users visiting /users/username/activity is. For most visiting their summary and invited page is.

Keep seeing the following error in the error logs in relation to these pageviews:

ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR:  column users.email_digests does not exist
LINE 1: ..." AS t1_r14, "users"."last_emailed_at" AS t1_r15, "users"."e...
                                                             ^
: SELECT  "directory_items"."id" AS t0_r0, "directory_items"."period_type" AS t0_r1, "directory_items"."user_id" AS t0_r2, "directory_items"."likes_received" AS t0_r3, "directory_items"."likes_given" AS t0_r4, "directory_items"."topics_entered" AS t0_r5, "directory_items"."topic_count" AS t0_r6, "directory_items"."post_count" AS t0_r7, "directory_items"."created_at" AS t0_r8, "directory_items"."updated_at" AS t0_r9, "directory_items"."days_visited" AS t0_r10, "directory_items"."posts_read" AS t0_r11, "users"."id" AS t1_r0, "users"."username" AS t1_r1, "users"."created_at" AS t1_r2, "users"."updated_at" AS t1_r3, "users"."name" AS t1_r4, "users"."seen_notification_id" AS t1_r5, "users"."last_posted_at" AS t1_r6, "users"."email" AS t1_r7, "users"."password_hash" AS t1_r8, "users"."salt" AS t1_r9, "users"."active" AS t1_r10, "users"."username_lower" AS t1_r11, "users"."auth_token" AS t1_r12, "users"."last_seen_at" AS t1_r13, "users"."admin" AS t1_r14, "users"."last_emailed_at" AS t1_r15, "users"."email_digests" AS t1_r16, "users"."trust_level" AS t1_r17, "users"."email_private_messages" AS t1_r18, "users"."email_direct" AS t1_r19, "users"."approved" AS t1_r20, "users"."approved_by_id" AS t1_r21, "users"."approved_at" AS t1_r22, "users"."digest_after_days" AS t1_r23, "users"."previous_visit_at" AS t1_r24, "users"."suspended_at" AS t1_r25, "users"."suspended_till" AS t1_r26, "users"."date_of_birth" AS t1_r27, "users"."auto_track_topics_after_msecs" AS t1_r28, "users"."views" AS t1_r29, "users"."flag_level" AS t1_r30, "users"."ip_address" AS t1_r31, "users"."new_topic_duration_minutes" AS t1_r32, "users"."external_links_in_new_tab" AS t1_r33, "users"."enable_quoting" AS t1_r34, "users"."moderator" AS t1_r35, "users"."blocked" AS t1_r36, "users"."dynamic_favicon" AS t1_r37, "users"."title" AS t1_r38, "users"."uploaded_avatar_id" AS t1_r39, "users"."email_always" AS t1_r40, "users"."mailing_list_mode" AS t1_r41, "users"."locale" AS t1_r42, "users"."primary_group_id" AS t1_r43, "users"."registration_ip_address" AS t1_r44, "users"."last_redirected_to_top_at" AS t1_r45, "users"."disable_jump_reply" AS t1_r46, "users"."edit_history_public" AS t1_r47, "users"."trust_level_locked" AS t1_r48, "users"."staged" AS t1_r49, "users"."automatically_unpin_topics" AS t1_r50 FROM "directory_items" LEFT OUTER JOIN "users" ON "users"."id" = "directory_items"."user_id" WHERE "directory_items"."period_type" = 4  ORDER BY directory_items.likes_received DESC, users.username LIMIT 50 OFFSET 0)
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-mini-profiler-0.9.9.2/lib/patches/db/pg.rb:90:in `exec'

Here's the backtrace.

/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-mini-profiler-0.9.9.2/lib/patches/db/pg.rb:90:in `exec'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-mini-profiler-0.9.9.2/lib/patches/db/pg.rb:90:in `async_exec'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/postgresql_adapter.rb:592:in `block in exec_no_cache'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/abstract_adapter.rb:472:in `block in log'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/notifications/instrumenter.rb:20:in `instrument'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/abstract_adapter.rb:466:in `log'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/postgresql_adapter.rb:592:in `exec_no_cache'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/postgresql_adapter.rb:584:in `execute_and_clear'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/postgresql/database_statements.rb:160:in `exec_query'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/abstract/database_statements.rb:355:in `select'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/abstract/database_statements.rb:32:in `select_all'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `block in select_all'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/abstract/query_cache.rb:83:in `cache_sql'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `select_all'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/relation/finder_methods.rb:356:in `find_with_associations'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/relation.rb:639:in `exec_queries'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/relation.rb:515:in `load'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/relation.rb:243:in `to_a'
/var/www/discourse/app/controllers/directory_items_controller.rb:49:in `index'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal/implicit_render.rb:4:in `send_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/abstract_controller/base.rb:198:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal/rendering.rb:10:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/abstract_controller/callbacks.rb:20:in `block in process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:117:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:117:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:555:in `block (2 levels) in compile'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:505:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:505:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:92:in `__run_callbacks__'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:778:in `_run_process_action_callbacks'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:81:in `run_callbacks'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/abstract_controller/callbacks.rb:19:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal/rescue.rb:29:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal/instrumentation.rb:32:in `block in process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/notifications.rb:164:in `block in instrument'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/notifications/instrumenter.rb:20:in `instrument'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/notifications.rb:164:in `instrument'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal/instrumentation.rb:30:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal/params_wrapper.rb:250:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/railties/controller_runtime.rb:18:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/abstract_controller/base.rb:137:in `process'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionview-4.2.5.2/lib/action_view/rendering.rb:30:in `process'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-mini-profiler-0.9.9.2/lib/mini_profiler/profiling_methods.rb:76:in `block in profile_method'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal.rb:196:in `dispatch'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal/rack_delegation.rb:13:in `dispatch'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_controller/metal.rb:237:in `block in action'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/routing/route_set.rb:74:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/routing/route_set.rb:74:in `dispatch'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/routing/route_set.rb:43:in `serve'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/journey/router.rb:43:in `block in serve'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/journey/router.rb:30:in `each'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/journey/router.rb:30:in `serve'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/routing/route_set.rb:815:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-protection-1.5.3/lib/rack/protection/frame_options.rb:31:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:186:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:164:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:186:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:164:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:186:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:164:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:186:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:164:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:186:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:164:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:186:in `call!'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/strategy.rb:164:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/omniauth-1.3.1/lib/omniauth/builder.rb:63:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/conditionalget.rb:25:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/head.rb:13:in `call'
/var/www/discourse/lib/middleware/anonymous_cache.rb:129:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/params_parser.rb:27:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/flash.rb:260:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/session/abstract/id.rb:225:in `context'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/session/abstract/id.rb:220:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/cookies.rb:560:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/query_cache.rb:36:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activerecord-4.2.5.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:653:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/callbacks.rb:29:in `block in call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:88:in `__run_callbacks__'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:778:in `_run_call_callbacks'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/activesupport-4.2.5.2/lib/active_support/callbacks.rb:81:in `run_callbacks'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/callbacks.rb:27:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/remote_ip.rb:78:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/debug_exceptions.rb:17:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/show_exceptions.rb:30:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/logster-1.2.0/lib/logster/middleware/reporter.rb:31:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/railties-4.2.5.2/lib/rails/rack/logger.rb:38:in `call_app'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/railties-4.2.5.2/lib/rails/rack/logger.rb:22:in `call'
/var/www/discourse/config/initializers/100-quiet_logger.rb:10:in `call_with_quiet_assets'
/var/www/discourse/config/initializers/100-silence_logger.rb:26:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/actionpack-4.2.5.2/lib/action_dispatch/middleware/request_id.rb:21:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/methodoverride.rb:22:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/runtime.rb:18:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/sendfile.rb:113:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-mini-profiler-0.9.9.2/lib/mini_profiler/profiler.rb:167:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/message_bus-2.0.0.beta.5/lib/message_bus/rack/middleware.rb:60:in `call'
/var/www/discourse/lib/middleware/request_tracker.rb:73:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/railties-4.2.5.2/lib/rails/engine.rb:518:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/railties-4.2.5.2/lib/rails/application.rb:165:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/railties-4.2.5.2/lib/rails/railtie.rb:194:in `public_send'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/railties-4.2.5.2/lib/rails/railtie.rb:194:in `method_missing'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/urlmap.rb:66:in `block in call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/urlmap.rb:50:in `each'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/rack-1.6.4/lib/rack/urlmap.rb:50:in `call'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/unicorn-5.0.1/lib/unicorn/http_server.rb:562:in `process_client'
/var/www/discourse/lib/scheduler/defer.rb:85:in `process_client'
/var/www/discourse/lib/middleware/unicorn_oobgc.rb:95:in `process_client'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/unicorn-5.0.1/lib/unicorn/http_server.rb:658:in `worker_loop'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/unicorn-5.0.1/lib/unicorn/http_server.rb:508:in `spawn_missing_workers'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/unicorn-5.0.1/lib/unicorn/http_server.rb:519:in `maintain_worker_count'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/unicorn-5.0.1/lib/unicorn/http_server.rb:283:in `join'
/var/www/discourse/vendor/bundle/ruby/2.0.0/gems/unicorn-5.0.1/bin/unicorn:126:in `<top (required)>'
/var/www/discourse/vendor/bundle/ruby/2.0.0/bin/unicorn:22:in `load'
/var/www/discourse/vendor/bundle/ruby/2.0.0/bin/unicorn:22:in `<main>'

Don't know if this is relevant data, but recently restored from backup about 72 hours ago.

Posts: 1

Participants: 1

Read full topic

GSoC Webhooks proposal draft

$
0
0

@fantasticfears wrote:

I finished my draft to the GSoC application for Webhooks.

Here I only includes technical details... Hope to hear your feedback :slight_smile:


Webhooks can be quite complicated if it aims ambitious. It's advisable to build the spec upon others' expertise (Thanks @tgxworld and @phanimahesh).

Spec & Scope

  1. A new panel at "/admin/webhooks" for manage webhooks. A table shows existing webhooks and “add new webhook” button in the panel. The table shows payload URL and event type of defined webhooks.
  2. Creating a detailed page for adding and editing a webhook. The form has 5 fields:
    • Payload URL: where Discourse can post webhook event to. Showing security warning if URL points to possible internal services;
    • Event types: when Discourse will trigger the webhook;
    • Content type: how Discourse encodes event payload;
    • Secret: an optional symmetric key for generating signature;
    • Cert verification: whether the POST verify the certificate;
    • Active: a checkbox to activate that webhook.
  3. Showing recent deliveries in the bottom of detailed webhook page under the settings. A modal about the delivery details will be shown when clicked.
  4. Adding a new job and a Sidekiq queue to deliver webhook events.
  5. Adding two models: Webhook and Event. A scrubber job can clean old events. Several event serializer is needed. Each event types have their serializers for extensibility (by plugin).
  6. Creating service hooks and slack webhook integrations.
  7. Sending the ping event when setup a webhook. It’s not stored in Event tables. It can be triggered by a special endpoint.
  8. Write documentation and rspecs.

Explanation

UI

Goal 1: Building a webhooks panel helps admin create and manage webhooks and they can also look into triggered event details. Webhooks are shown in the table where admin can easily identify the payload URL, associate events, active status and recent failed delivery if any. The implementation follows REST guidelines as the same as the other admin endpoints. It will be possible to access webhooks by API.

Goal 2: Detailed webhook management is a fundamental feature. Setting payload URL to a internal service may cause unintended behaviour, such as a URL pointing to /resources/delete may trigger destructive commands when a webhook is triggered and the POST request is sent. It’s better to warn the admin if Discourse find the payload URL pointing to common URLs for localhost.

Event type defines when the webhook will be triggered. Very common events such as new topic and new post can be listed as a option. But a checkbox list of all event is still needed. A wildcard event (*) is the most special type of event which will be triggered when any event is triggered.

A secret token is used to generate a digital signature to the payload. Therefore, a receiver can verify the authenticity of payload. (Same as API key or OAuth client secret)

Goal 3: Recent deliveries (events) table shows the status information and serves as a debug control panel. The table shows 30 recent events. When the admin scrolls down, Discourse loads more events in that table. Only recent 300 events can be found. Events older than 90 days will not be included (even if the total number of events in the table is less than 300). The table includes event’s status, UUID and created time.

When clicked an event, a modal shows the request and response including HTTP status code, headers, time spent for request and payload. Beyond that, a "redeliver" button shall be there to post the exact delivery.

Event endpoints are open for admins following REST style. Subscriber who may be interesting in operating actions to Discourse and who developed a offline recovery mechanism can access event endpoints by API key.

Model

Goal 4: events are delivered in the background job. And the job should be in a priority queue. On the one hand, it’s unacceptable to penalize request latency. However we still need to deliver the payload in real time. Therefore a sidekiq queue which has higher priority than default can help to avoid queuing after other jobs. On the other hand, a delivery may fail, a backoff and retry mechanism is better to be put in the background job. When getting lots of failure, the webhook will be disabled.

Goal 5: it’s evidently we need the Webhook model. Moreover, events are also keep in the database since it enable both automatic and manual retries and query in the admin panel. Old events can be cleaned by a scheduled background job if needed. Event serializers are new serializers by extending the existing serializers. There should be several options to control the serializable fields for plugins (services).

Goal 6: I will work on so called service hooks and at least a demo Slack service. Such a service creates the webhook for admins under the hood. It can be a gem of integrated services collection for Discourse. Or it can be several API to enable plugin authors to create new webhooks.

I need further investigations to find the best way to do the service integrations. Because once the ship sails, it’s hard to turn it back. If it’s not clear, it will cause a lot of pain to maintain the code. For example, it’s such a painful thing when create login authenticators…

Goal 7: A ping event is much helpful for debugging purpose while it’s not a real event. It shall not be persistent at all.

Summary: At a glance, testing endpoints, UI works and detailed documentation is trivial work. Detailed specification, serialization schema and hook point are a back and forth effort. They would take a lot of time and discussion.

Deliverables

  • Many pull requests towards the complete project
  • Code in a working branch at least
  • Documentation about webhooks for Discourse as well as a testing guide
  • Sample applications which receive and respond Discourse webhooks event
  • Service integration plugins, including Slack integration.

Groundwork

I conduct a small survey about how GitHub and Slack implement webhooks and tried them to see how it works. REST Hooks also have a lot of insights about webhooks.

GitHub’s webhooks practice

GitHub has a clean RESTful API for webhook. In particular, it also provides event logs. If subscriber went down, they can still reach event endpoints to deal with backlog. I think its approach is clean. I followed a lot of its practice in the proposal.

PubSubHubbub

GitHub can also work as a PubSubHubbub (spec). It’s a generic protocol which allows data producers and data consumers to work in a decoupled way. Subscribers who may interested in push events, can request hub to register. Once there is a new push to a repo, the hub announce the updates to subscriber. It’s similar to webhooks but a different protocol.

It’s typically used for content generators to deal with contents. Google offers a example hub. There is also a Wordpress plugin.

But it’s not fit into Discourse. We can subscribe to hear updates MessageBus in real time. Several API and Atom feeds can be used for polling. Webhooks cover the use case of it. Adding the hub will cause maintenance cost and learning cost to plugin authors. Thus I dropped this thought.

Slack’s webhooks practice

Slack has built two types of webhooks. One push out the chat messages while the other one post chats in the channel. A special hook called slash command will push out raw message and receive formatted response to beautify the chat message. I think it’s redundant since Discourse needs a lot of fields to create a post. It’s better not to maintain incoming webhooks. At least, it should just a proxy to internal manager. The subscriber can do anything they want when they receive an event.

REST Hooks practice guidance

REST Hooks list out several considerations about webhooks and REST style. I generally followed most suggestions in my proposal including subscribers (webhooks) resource endpoint, event resources endpoint, queuing mechanism and retrying mechanism. I leave batching and skinny payloads aside since I think a forum won’t have a huge amount of updates emitting at a fairly short time window. And skinny payloads ask the subscriber to request again which is somehow painful.

GitLab implementation

GitLab is an open source Rails app built the GitHub experience locally. It implements the webhooks. They built several hook models based on single table inheritance. They are invoked in different levels. ProjectHooks will be triggered when a push happened to a repo. SystemHooks is used to notify when a new repo created.

It’s easier if we don’t distinguish the event types as a model. Because many events in Discourse could be general interested in external services. There isn’t a huge layer like GitLab. By all means, we can add new attributes for scoping than creating a new model.

Event serialization

For maximizing extensibility and security reasons, a series of event serializers is created.

Webhook defines triggered events, admins have to set them in the admin panel firstly. These events can be Topic status, Topic operations and etc.
Triggered event have a attribute called action, used to present what is really happend.

Let’s define some events to get started:

  • EventSerializer
    The base serializer for event. I think it's better to put them in the header. Including:
    • event_type: match the event type you set on the webhooks panel
    • action: a string consisting of resources and actions, e.g. topics.{created, auto_closed}
    • version: Discourse version
    • uuid
  • TopicEventSerializer
    For several actions, can be status events and operation events.

    • Status events include: topics.{created, status_updated, operated, pinned, unpinned, archived, closed, invisible}.
    • Operations event include: set_auto_closed, post_owner_changed, timestamp_changed.
    • Attributes
      • topic: id, title, fancy_title, posts_count… (basic everything from topic_view), but including pinned, pinned_globally, unpinned, pinned_until, details, highest_post_number (not including attributes for specific user)
      • category: category_id will not be included in topic, but as a object
      • user: same as category
  • MessageEventSerializer
    Same idea as topic but for private messages: private_messages.{created, replied}.

  • InviteEventSerializer
    For several actions: invites.{created, accepted}.

  • UserEventSerializer
    For several actions: users.{created, activated, blocked}.

  • PostEventSerializer

    • For several actions: posts.{created, updated, deleted}.
    • Excluding attributes for specific user.
    • Including topic, category, user hash.

HTTP request

  • Faraday uses middleware like interface to build the request. Though the last commit happened on Dec, 2015.
  • HTTParty. Under active maintenance, can be wrapped in a class.

Generally, I don't have a preference. The team maintains discourse_api gem which uses Faraday. Say, it's better to save learning cost.

Stub implementation

See: https://github.com/fantasticfears/discourse/compare/86d2773d3a73c63dd57c180a4de16310014af307...5dcfc01

Posts: 2

Participants: 1

Read full topic

Unable to pinch zoom on ASUS Memopad 7 running Android

$
0
0

@Rob_Nicholson wrote:

I'm unable to pinch zoom on my 7" tablet and I find the font just a little too small for my weary eyes. I do wear specs on the PC but I'm normally fine on here and my phone. Is there a way to either enable pinch zoom or maybe bump up the fonts by a few points?

Posts: 11

Participants: 4

Read full topic

Viewing all 60599 articles
Browse latest View live




Latest Images