Jump to content
View in the app

A better way to browse. Learn more.

Learn more

Invision Community Blog

We refer to the icons you can use to post to third party services such as Facebook, Twitter and Digg as "Sharelinks" in our software suite, and we consider these an important tool both for community promotion and for search engine optimization. Not only can you get links to your community out there on other large, popular sites on the internet where you might be able to drive back more traffic to your own site, but search engines will also see these links to your site on the other large popular sites, follow them, and rank the pages they reach accordingly.

Share links were supported in 3.x, however while planning 4.0 we felt that the implementation could be improved, and so we've done just that. We have tidied up the interface to make managing the services easier for the average admin, provided more flexibility for third parties who wish to developer additional share link services, and we've made it much easier to use from a development stand point.

Managing Sharelinks

Within the admin control panel, you must be able to manage the sharelink services that are available. You may only want to offer buttons to share to Facebook and Twitter, removing clutter that you don't feel your community is familiar with. Or you may want to enable every share service link possible, because your community is full of technical-oriented users who will make use of them. Wherever you fall within this spectrum, the process should be simple and straightforward.



We have removed the ability to add and delete sharelinks, because the reality is most services require far more integration than simply specifying a URL. Thus, we have made the system plugin-based, which allows developers who are familiar with the process far more control, while greatly simplifying the interface for the average administrator. If you are comfortable with adding a link and an icon, building a plugin (see below) is a very simple process. Otherwise, if you don't know what an "HTML" is, you will now be able to work with this page in a far easier manner.

There is a generic settings button at the top, which allows you to globally enable and disable share links, as well as specify whether you want users to have the opportunity to automatically share links to new content with supported services. When you choose to "Edit" a given service, you can specify which groups can access that share link, and specify any service-specific settings related to that service (for instance, to change the default Twitter hash tag used when sharing via Twitter, you would click to edit the Twitter service).


Integrating a New Service

While we ship with some of the most popular services already supported (including support for LinkedIn sharing, new in 4.0!), it is a very simple process to add support for new services. When you build a new sharelink service and drop it into the appropriate folder, it will automatically be installed as soon as you visit the sharelinks management page in the ACP, where you can then choose to enable/disable the service and configure any available options. If you delete a plugin file, the service will automatically be deleted again when you visit this page in the ACP. No further interaction is required to install or uninstall a service, other than visiting the management page in the ACP.

The basic template for a plugin file is as follows

<?php namespace IPSContentShareServices; class _Servicename { /** * @brief URL to the content item */ protected $url = NULL; /** * @brief Title of the content item */ protected $title = NULL; /** * Constructor * * @param IPSHttpUrl $url URL to the content [optional - if omitted, some services will figure out on their own] * @param string $title Default text for the content, usually the title [optional - if omitted, some services will figure out on their own] * @return void */ public function __construct( IPSHttpUrl $url=NULL, $title=NULL ) { $this->url = $url; $this->title = $title; } /** * Add any additional form elements to the configuration form. These must be setting keys that the service configuration form can save as a setting. * * @param IPSHelpersForm $form Configuration form for this service * @return void */ public function modifyForm( IPSHelpersForm &$form ) { } /** * Return the HTML code to show the share link * * @return string */ public function __toString() { return "<b>Some HTML</b>"; } }
The class name "_Servicename" should be the same as the filename, minus the _ (the underscore is part of hooks and plugins system in 4.0, which you can read about in a later blog entry). So this filename would be "Servicename.php" and would be located in /system/Content/ShareServices/. The modifyForm() accepts a reference to the form object that is displayed when configuring the service. Here, you can add new configuration options that will be saved automatically (the configuration options should be settings). If you need to specify any HTML or CSS that needs to be manually loaded for the service, you can add assets to both the <head> of the document, or insert content right before the closing </body> tag, depending upon the service's needs in the constructor. The __toString() magic method should return the HTML necessary to generate the share service link.

It is really that simple - there are no hidden "gotchas" to watch out for.

As an aside, the built in ability to email a link has been greatly simplified for third party developers as well. If you implement third party applications by extending the appropriate classes (which we will blog more about later), the ability to email a link to your page with permission checking built in will all be handled automatically for you. You don't need any plugin files specifically to facilitate emailing links to your content items in 4.0.


Implementing Sharelinks in Applications

To implement the sharelinks in your third party applications is equally simple. As mentioned above, assuming you extend the appropriate classes to generate content items (which will be blogged about in detail at a later date), you need only do two things to implement share service integration in your application.

First, your class that extends IPSContentItem must implement the interface IPSContentShareable. This automatically enables the share links in your application and adds support for any functionality the share services utilize.

Second, your template should include the appropriate sharelinks template where-ever you wish to display the share links

{template="sharelinks" group="global" location="front" params="$contentItem"}
If you wish to render the share links in some manner beyond our default generic rendering, you can simply use whatever HTML you need to, however we feel that most content items can utilize our generic template just fine.


A Few Notes in Closing

We feel that these changes will make implementing share links as easy as possible while still providing you all the flexibility you may need to customize the interaction and display of the links at every level.

We have also decided to remove the "print" and "download" share links at this time. In researching their usage and their functionality, we feel that it is no longer worth maintaining these unnecessary links which simply add clutter to the page. All browsers allow you to save a page (i.e. download it) already, and all browsers allow you to print a page already. We will include a print stylesheet with 4.0, just as we did with 3.x, so when you choose to print a page the output will be better tailored for printed media. We feel, however, that retaining the links adds no real value for the vast majority of users, and is more inline with what most users encounter on other similar websites.

We hope that the improvements and decluttered interface, both on the front end and the ACP, make configuring and using these services (and adding support for others) much simpler than in previous versions.
  • 9,340 views
In 3.x, we support HTML emails being sent by the software. However, due to constraints we had at the time, HTML emails use pretty much the same content as plain text emails, but wrapped in a simple HTML wrapper. Additionally, users had to explicitly decide whether they wanted to receive HTML or plain text emails via a preference setting - quite an anachronism. All in all, not a very satisfactory user experience.

Email handling in 4.0

In 4.0, users no longer choose which type of email to receive. Our email handler sends both types in a single email, and the email client chooses the most appropriate to show based on its capabilities. If it can display a fancy HTML version, that's what they'll see by default, but plain text is used if not.

Email template system

In 3.x, email content is defined by the language system, and each email has one language string which forms the content for both the plain text and HTML versions. Clearly, if we were going to improve the HTML templates we ship with, this would have to change.

In 4.x, each type of email has two templates - one for HTML, one for plain text. This means a better display of content can be created for HTML emails, while keeping the plain text ones simple and to the point. Email templates make use of the skinning system foundation (which we'll reveal later), meaning they have full use of logic, template tags and more - so we can also customize the emails depending on the user they are being sent to (note though that email templates are not per-skin; they are global to the site). And, of course, email templates can be added and edited via an interface in the AdminCP. This isn't groundbreaking stuff, but a vast improvement on email handling in 3.x.

Email template design

We also wanted to improve our email templates, so that each type of email sent was designed specifically for the purpose. The data shown in a registration email will be different to a topic digest, for example, and the email should reflect that.

Coding email templates is not a trivial thing, unfortunately. The latest version of Microsoft Outlook uses the Microsoft Word rendering engine(!!), while GMail strips out all CSS included in style tags - and that's just the start of the gotchas. This makes designing email templates a tricky business, and one that requires lots of testing to ensure compatibility. For our first 10 templates alone, I reviewed 900 screenshots to spot problems.

As a result, we've taken the approach of creating email templates which are simple in appearance and would work well for most sites, with the goal of hopefully avoiding the need of most sites to edit them at all (though you can, if you wish). The colors we've used are fairly neutral, for this reason.

For those mail agents that are a little more... advanced, our email templates in 4.0 will be responsive. They will look great on mobile devices as well as desktop clients.

I have included some examples of email templates, along with their mobile counterparts. I should note at this point that this does not reveal the main skin design. As discussed above, emails are intentionally separate in design.

Admin-completed registration




Friend request




New personal message




New profile comment



  • 17,106 views
We've been hard at work on IPS 4.0 for some time now, and we're finally at a stage where we are ready to reveal the new AdminCP to you. I won't be showing you everything the ACP has to offer - some things will be revealed in more detail in later blog entries. But lets get to an overview.

Background information

IPS4 brings with it a new CSS framework that aims to modularize our styles. This is something we started to work towards in IPB 3.2, but at that time we couldn't completely replace our structure. We no longer have a monolithic ipb_styles.css file. We now have a bunch of small CSS files, and each one handles something in particular. There's one each for forms, tables, pagination, buttons, layout and so on.

This brings a few key benefits. Firstly, when we need to make a bug fix in, say, the forms CSS file, IPS4 will still be able to automatically upgrade all the other css files for you. In 3.x, one bug fix in ipb_styles.css could mean the whole file had to be manually upgraded. Secondly, it will be a lot more obvious for skinners where to look for particular things. Need to style a button? Look like buttons.css. Easy. And thirdly, if you're building pages in IP.Content, and you want to use our button styles, you can simply include that one CSS file without needing to include the entire CSS framework.

CSS is of course concatenated and compressed before being delivered to the browser, but in a development environment, it exists as I described it above.

In IPS4, both front end and AdminCP share the same CSS (and Javascript) framework. Skinners will be able to ship skins that work on both the front end and AdminCP with only a little extra work - and, of course, when we make bug fixes to the framework, it'll fix both areas.

Before we go further, I want to make this part clear: The front-end and AdminCP look different. What you'll see shortly isn't what the front-end looks like. We will reveal that separately later. While the same framework is used, the AdminCP extends and overrides parts of it to suit its needs and style.

Goals

What did we want to achieve with the AdminCP? Our current AdminCP is often regarded as the best out of the big forum software platforms, so redesigning is a big undertaking.

Better user of space. Our current ACP uses vertical space for the main menu, and horizontal space for the application menu. In an era of widescreen desktops being standard, this could be improved. Get rid of dropdown menus. The main menu currently uses dropdowns for navigation, but this can be difficult to use - especially if you want access something in a 3rd party app, meaning you have to traverse the Other Apps menu. More consistency across pages. Our current ACP has some interactive tables (e.g. the member list) - but not every table makes use of the functionality. We should be enhancing every page with similar functionality, if it makes sense. Better styling. People aren't a fan of pink, it turns out. I guess it'll have to go. The blue gradients are showing their age too. And the big one: Better mobile support. You can't effectively use the AdminCP on a mobile device. It's time you were able to manage your entire community from your phone with all of the same functionality, right?


Responsive by default

That last one is what we're most excited about. The AdminCP in IPS4 is fully responsive, and allows you to do everything just on a phone or tablet. What is responsiveness? It means that the page automatically changes to better suit the device you're using. While a desktop user would see full navigation menus and tables of data, a mobile user will see a reduced view (but with all the same data present!). Whether you need to manage your members, change some settings, send a bulk email or run some diagnostics, it can all be done on the go. This is a first for the big community software platforms, as far as I'm aware.


Preview

Here is a sample page from the new AdminCP, as seen on a desktop, with the same page shown at a mobile resolution:



Although I won't include it here, tablets will see an 'intermediate' view with a reduced menu on the left. So, let's go over some of the key features of the screenshots.

Navigation

First, and perhaps most importantly, is the navigation. On a desktop, your applications are now arranged down the left-hand side, with their respective section menus available simply by hovering on the application - no dropdown menus to traverse. The application menu can be reordered per-admin, allowing each staff member to set the menu up to best suit their role.

On a mobile, there's obviously not the space for a wide navigation menu. Therefore, the application/module menu is activated by clicking the top-right icon. This opens a sidebar, from which you can navigate:



Tables

What you see in the screenshots are our new default way of displaying tables of data. On the desktop view, we have filters across the top, a search box (and advanced search popup), and table headers can be clicked to dynamically sort the data via ajax.

On a mobile view, this all collapses down - filters and sorting become menus, while table rows collapse to show data in a more suitable view. Responsive tables are a tricky thing to do right and there's a few different approaches, but given the types of data our AdminCP tables typically show, we think this is the best approach for us.

Forms

As has been discussed in some of our developer blogs, the IPS 4.0 framework supports a wide range of form field types - everything from text inputs to tree selectors to matrices. All of these field types work both on desktop and with a responsive mobile view.

Here's a simple AdminCP form on both desktop and mobile:



Tabs

Tabs are used extensively, where appropriate. Here's a screenshot showing a typical tabbed page (and it also shows a tree view):



Video of the mobile view in action

I've taken a short video of the member section in action, showing filtering, live searching and the advanced search popup. I'm using the iOS simulator here, which has some display jitters and requires me to use the mouse, but it should give you a good idea of how the AdminCP will work on a phone.



Conclusion

So there we go - an overview of the new AdminCP. We still have more to show you. Individual features and pages that are noteworthy will be blogged about in due course in more detail, so keep an eye on this blog and our developer blog for more.

Please do bear in mind that this is pre-alpha software, and everything you see is subject to change. We look forward to your feedback!
  • 33,241 views
Introduction

Modifications, add-ons, plugins, hooks - whatever your preferred name for them is - 3rd party code modifications are an important part of any successful web application. It wasn't that long ago that the way you did this was manually opening up files and copying and pasting bits of code in, or the really cool web applications had points scattered throughout the code for modifications to be injected into, or even scripts which opened up the files and made the changes for you (I'm not joking, that's seriously what used to go on!). In fact, IP.Board was one of the first web applications to, using OOP, support modifications in a more structured way.

Currently, we largely have 2 types of modifications: applications, which add whole new areas and functionality to your site (all of our applications: IP.Blog, IP.Gallery, IP.Downloads, IP.Chat, IP.Content and IP.Nexus use this architecture) and hooks which modify or extend the functionality of the IPS Social Suite or of applications.

Applications themselves are sort of self-governing so there isn't much to say about them, with one exception: applications will now be able to be downloaded and subsequently installed into your Admin CP as one file - you will not have to FTP upload application source files. The file will just be a regular .tar file, so course, if you were so inclined, you could open it and go old skool.

For the rest of this blog entry, I'm going to focus on hooks. Though parts of this blog entry will be more technical in nature than our others, I've tried to keep it just to what everyone will be interested in, and leave the boring stuff until the end.


Terminology

The term "hook" in 3.x is ambiguous. Sometimes it refers to the whole thing (e.g. "install a hook") and sometimes it refers to a specific technical part of that - the code which overloads other code (e.g. "skin hook", "library hook"), which are, even more confusingly, sometimes called "hook files".

In 4.0, we've decided to rename hooks to plugins. The technical parts which make up a plugin will still be referred to as hooks.


Sandboxing

Plugins, by their nature, extend functionality already present on your site. Up until now, if a plugin experiences a problem (for example, if a new version is installed which the plugin doesn't support) it can cause an error on your site, which disabling the plugin fixes.

Starting in 4.0, plugins will be sandboxed. This means that if a plugin experiences an unexpected error (such as a database driver error), your site will automatically fallback to the default behaviour, and your users will never know anything went wrong.


Simple (yet advanced) settings

In IP.Board 3.x, the Admin CP maintained a massive central area for managing most (though not all) settings. Plugins could add settings to this area, though there was no real standard to where to do that. Also, because this area was separate from the area where you install plugins, it could sometimes be confusing how to configure a plugin after installing it.

In 4.0, each plugin is allocated a settings page which is accessed just by hitting the "Edit" button on the list of plugins. Plugin authors can manage this page how they like - rather than being confined to the strictly tabular layout and specific input types in 3.x.


Versioning

In 3.x, unlike with applications, there was no particularly clear way to upgrade a plugin from one version to another. In 4.x, plugins now support full versioning, so you can just upload a new version, and an upgrader will take care of it.


Hook Types

In 3.x, there were several different underlying types of hooks:
Action overloaders - which allowed overloading the PHP class for any controller. Library hooks - which allowed overloading the PHP class for some (though not all) other classes. Data hooks - which allowed the modification of variables at specific, defined places in the code. Skin overloaders - which allowed overloading the compiled PHP class representing a group of templates. Template hooks - which allowed content to be inserted at specific points in templates.


For 4.0, we've made some quite radical changes:

Code Hooks

The first 3 have been merged into one concept we call "Code Hooks". Code Hooks can overload any class (even things which presently can't be overloaded like extensions) through a technique called monkey-patching (more details have been mentioned in the developer channel). This, combined with the use of Active Record models for all content items (so "Topic", etc. is a class that can be overloaded) also makes data hooks obsolete.

Theme Hooks

The last 2 have also been merged into a concept called "Theme Hooks" (we're also renaming "skin" to "theme"). The way the current template hooks work is to insert content around certain pre-defined tags in the template. The problem is, not always is the point the plugin author needs available, also this is done in a way the content being inserted isn't aware of it's surroundings, which makes it difficult for things like adding a button to every post, which would need to know information about that post.

After thinking for ages about a better way to facilitate theme hooks (I was halfway through a system which injected hook points automatically at compile time), our designer Rikki reminded us that a pretty well-known method for selecting HTML elements already exists... CSS selectors.

Video demonstration

What's really cool about this is that the content used acts as if it was part of the template - if for example, it's inserted in a foreach loop, the variables created by that are available. It can also use template logic and everything else templates themselves can do.

On the back-end, these are compiled into a file which behaves like a 3.x skin overloader - so if it is necessary (or just desired) to overload the compiled version of the template, that is still possible.

Theme hooks work for the Admin CP as well as the front-end.


Developer information

Developers no doubt would like to know the technical information of how this all works. Rather than write a blog entry covering all the different parts of plugins, we thought you might be interested to just see the developer documentation. We have 2 articles we can show you - one covering all the technical details of plugins, and another which provides a step-by-step guide for how to create a plugin.
  • 12,069 views
** We have enough applications to fill this position. Thank you for your interest! **

Piracy is something that all software companies face and IPS is no exception. Our losses due to credit card fraud and software piracy are significant and to minimize passing along costs to customers, we are seeking to expand our piracy department and take a harder stance against piracy and pursue those who engage in it.

The position entails:

- Identifying customers, using internal tools, that have inactive licenses and are using later versions of the software than their license allows and report to customer service for license termination.
- Identifying customers, using internal tools, that have shared IPS products or marketplace purchases with illegal download sites and report to customer service for license termination.
- Following up on usage piracy complaints.
- Vigorously pursuing distribution hubs.
- Working with web hosts, ISPs and law enforcement.

To qualify you MUST:

- Be at least 18 years old (for legal reasons, no exceptions to this policy can be made.)
- Have excellent written communication skills. English is a must.
- Be familiar with identifying the owner and host of a website (i.e.: Using WHOIS and other similar tools.)
- Be familiar with the DMCA and associated procedures.
- Reside in the United States.

** We have enough applications to fill this position. Thank you for your interest! **

Thank you for your interest!
  • 18,202 views
One of the things we wanted to focus on for IPS Social Suite 4.0 right from the beginning was providing better support for sites which do not use English or use multiple languages (or, as it was scribbled on my whiteboard, "++ i18n/L19n"). In this blog entry I'm going to cover some of those changes and new features.


Translatable Everything

Currently when you create a forum, user group, custom profile field, etc. you have to give it a title and can only do this in one language. If you have more that one language installed, you might want to provide different titles for different languages.

In 4.0 you can do exactly that - if you have only one language installed, these fields will continue to show as normal text boxes - however, if you have more than one installed you'll see several text boxes like this:



Visual Language Editor

One feature that has been really popular in IP.Board is the Visual Skin Editor - a tool which allows you to browse your site, and click on elements to bring up a colour selector to change it. What if we could take this idea and apply it to translating as well? Allowing you to click on any word or phrase on your site and translate it there immediately. In 4.0, you can.


Easier Language Management

In addition to the visual translation we've also made several improvements to the traditional translation method:
As you search for a language string, results appear as you type. Editing a language string saves immediately without needing to click a save button. Filter tabs can show you words/phrases which have not yet been translated or the translation is out of date (meaning we've changed the default English value for the word/phrase since it was translated).

We've also made importing/exporting much faster and more reliable - no matter how large your language is (it will grow as you add more applications of course) there is now no risk of hitting an error importing/exporting (for those interested in the technical side of how this is achieved, see this blog entry).

An exported language pack will also now maintain information on the version of each application it was exported from, so that the filter which shows outdated language strings is always accurate.


Automatic Language Detection

Let's say you have Spanish and French languages installed on your site - up until now, you'd have to choose one default language, and users who want the other would have to manually choose it (which can be extremely difficult to find how to do when you're browsing a site in a foreign language).

In 4.0, we automatically examine the information that the user's browser sends (which includes their preferred language) to choose the best one out of what's available, if that user hasn't already set an explicit preference.

Pluralisation

In English, pluralisation is very simple - for most nouns, you just append "s" on the end, with some variation for certain words.

This however, isn't the case in all languages - for example, I was speaking with the owner of a site in Slovak recently who was telling me that the word "records" changes depending on the number of records there are - for 2 records, it's "2 články", but for 5 records it's "5 článkov". Currently, most language strings only have a singular and plural form (as is all that's needed in English) - meaning having the site show "2 články"/"5 článkov" was impossible.

In 4.0, we've introduced some really basic logic into language strings to accommodate this. Rather than having, for example, two language strings with the singular and the plural, there is now one with a value like this:
{# [1:record][?:records]}
The # indicates where the number will go, then each set of square brackets represents a possible value - the number before the : indicating the number which will cause that to show, and ? meaning "all other numbers".

So for our Slovak example, we'd set the value to:

{# [1:článok][5:článkov][?:články]}
On display, it will automatically show the appropriate version.


Lists

Along a similar thread to pluralisation, we've also made the way lists are formatted to be customised through a special language string. For example, a list in English looks like "one, two and three". However, in Japanese, it's "一、二、三。" (the comma symbol is different and there's no "and") - similarly Arabic, Thai and others have similar differences. In 4.0, simply by changing an example language string, this can be changed.

In the default language, this language string is:

a, b and c
For our Japanese example, we'd just change it to:

a、b、c

UTF-8

Without wanting to get into too much technical detail - UTF-8 is the most common of many ways text can be encoded for storage and display on webpages. UTF-8 has been the default encoding in our software since IP.Board 3.0.

Some sites which have been around for a long while though may not be using UTF-8. This can cause issues with some features where UTF-8 encoding is expected (for example, many features which rely on JavaScript require UTF-8 due to JSON only supporting it and nothing else). In addition, some sites may try to use UTF-8, but content is actually stored differently as the database is set to a different encoding, which can also cause issues.

In 4.0, we're going all UTF-8. If you're not already on it, the upgrader will convert data. This means a much more reliable and compatible way of handling text.
  • 28,144 views
The IPS Social Suite stores skin and language (and some other stuff) in xml files which are imported into the database at installation and upgrade. The reason we do it this way is so of course, you can export skins and languages and install them on other sites or distribute them via the IPS Marketplace.

I'm not the biggest fan of PHP's XML handling at the best of times (it would seem whoever wrote the SimpleXML class and I would disagree on the definition of "simple") and had already changed some of the places we use XML to use JSON instead, but these skins and languages are particularly difficult because they can get huge. Handling these large files, especially when combined with sub-par servers can lead to memory exhaustion or maximum execution time timeouts. Our current method was taking both too much memory, and too much time in a single HTTP request.

So we had to come up with something better. At first we considered splitting the XML file into several, but that would mean either requiring people installing a new skin/language to upload multiple files (which we deemed not acceptable) or compressing them in some way (which is another of PHP's weaknesses, and would have just created another problem). We also researched different markup languages, however found similar problems with everything. We also had to make sure that any solution didn't step on the toes of any of our other 4.0 goals, in particular we want to completely eliminate writing cache files to disk so as to better support cluster environments.

The solution was two-fold. To resolve the memory problem, we decided to use the XMLReader and XMLWriter classes - these are non-cached, forward-only classes for reading and writing XML. Rather than store the document in memory, you can only read/write one node at a time, and can only move forward, not backwards. Even with this approach though, we needed to account for the fact that importing is an intensive operation anyway, and one that needs to be staggered. To accommodate this, we wrote a simple AJAX-based "redirector", which continuously fires HTTP requests at a PHP controller until the controller reports the import is finished (it displays a fancy progress bar to the user while this is happening).



We decided to make the AJAX redirector a helper class so that it can be used elsewhere and by third-party developers (rather than our current approach in some areas of making the user sit through loads of physical redirects). The code is really simple:

IPSOutput::i()->output = new IPSHelpersMultipleRedirect( /* Query string which will take us back to this controller */ 'app=foo&module=bar&controller=baz&do=process', /* Code to run for each HTTP request */ function( $data ) { // If we're not done yet... return array( $data, // Data to get passed back to this function, "Processing...", // Message to show (via AJAX) to the user 50, // [Optional] Number between 1 and 100 to indicate progess for progress bar ); // If we're done return NULL; }, /* Code to run when we're done */ function() { // Code that runs when we're done } );
  • 9,115 views
We have a few updates to our services to share with you.

Community in the Cloud

For over 11 years IPS has provided hosting services for clients that want a turn-key approach to their online community. Over time we have become more and more focused on community hosting solutions so it seemed like a good time to drop the older "hosting" term and adopt a new name for our service: Community in the Cloud. Granted it's the buzzword of the day but we were in the cloud before the cloud was a term :smile:.

Right now it's all that you had before but presented in a much easier to understand format. Check out our new information page: http://www.invisionpower.com/cloud-pricing

This name and presentation change is just step one. We will soon be increasing our storage quotas and have some other great changes on the way!

New Support Package

We often get clients who are looking for a higher level of support beyond just tickets. They want training, schedule upgrade service, consultations, and more. Of course offering that level of support is intensive and in the past we have always custom-quoted such services. Now to streamline we we have created a new Premium Support package that includes:

Implementation
Scheduled installation time Initial training & consultation by phone or live chat Post-deployment best practices training Custom migration from other platforms* Custom skin design* Custom single sign on (SSO)*

SupportSame business day ticket response Scheduled upgrade times Custom skin upgrades between versions* Security updates applied before public release

Monthly MaintenanceLogs checked for signs of problems Advise and schedule if upgrades are available Database maintenance Settings reviewed for optimal performance Best practices reviews

* Custom services may incur additional fees

The new Premium Support package is $500 every 6 months and is available for purchase or upgrade today. If you have any questions feel free to email [email protected] and we will be happy to help.

Transfer Promotion

If you are interested in moving to IPS Community in the Cloud we are offering a promotion that should make now the best time to make the switch.

From now until 1 September 2013 we will offer free transfers and free conversions. This means if you are already using IPS Community Suite on your own servers but want to switch to the CiC we will move your data for you. It also means that if you are using a different community software provider and are ready to upgrade to CiC we will both transfer your data and convert it using one of our pre-made converters.
  • 12,577 views
Charts and graphs are an essential tool in modern web applications. The API we use for displaying charts and graphs in IP.Board presently is something we wrote in-house during IP.Board 2.x - it uses the PHP GD library to generate an image representing a chart. At the time, it was pretty amazing, but as times have changed a number of libraries for generating much more visually appealing and interactive graphs have emerged. For IPS Social Suite 4.0 we've decided to retire our GD-based charting library and embrace something a bit more modern and familiar for developers to work with.

After looking at a number of different solutions, we decided to use Google Charts (although wrapped with a simple gateway PHP class). Google Charts look great, have great features, are commonly used (so we figure any third-party developers who want to add charts to their apps will be able to do so easily) and is a service provided for free with no API limitations.


I'll show you an example of how simple it is to create a chart in IPS Social Suite 4.0. Let's look at the code I might use to show a graph of the number of members registered over time:

/* Init Chart */ $chart = new IPSHelpersChart; /* Specify headers */ $chart->addHeader( "Date", 'date' ); $chart->addHeader( "Members", 'number' ); /* Add Rows */ $stmt = IPSDb::i()->build( array( 'select' => "COUNT(*) AS count, DATE_FORMAT( FROM_UNIXTIME( joined ), '%Y-%m-%d' ) as joined_date", 'from' => 'core_members', 'group' => 'joined_date', 'order' => 'joined DESC', ) ); $stmt->execute(); while ( $row = $stmt->fetch() ) { $chart->addRow( array( new IPSDateTime( $row['joined_date'] ), $row['count'] ) ); } /* Output */ IPSOutput::i()->output = $chart->render( 'LineChart', array( 'title' => "Registrations", ) );
As you can see, the code is extremely simple to understand and use. This is what the output looks like:

By hovering over any point I'll see a tooltip with the value at that point.

All of the chart types and options available in Google Charts are available to us. Let's make some changes to make the lines curved, get rid of the legend and show titles on each axis - I just change the last line of the code to:

IPSOutput::i()->output = $chart->render( 'LineChart', array( 'curveType' => 'function', 'hAxis' => array( 'title' => 'Date', ), 'legend' => array( 'position' => 'none', ), 'title' => "Registrations", 'vAxis' => array( 'title' => 'Number of registrations', ) ) );
And now my chart looks like this:

  • 12,468 views
Javascript is a key component of front-end web development - it's essential for a modern web app to provide a good user experience, and javascript is central to enabling that. Getting it right in 4.0 has been one of our goals from the start.

Problems

To begin, let's summarize some of the issues with javascript in the current 3.x series:
Lack of file organization (a single JS directory that contains dozens of unrelated script files) Different script types are bundled together (interface widgets, routes and full applications all in one place) Lack of modularity (each JS file is pretty much a floating island in how it might implement functionality, with no formalized structure) Simple things requiring code (how many times have you had to write half a dozen lines of JS just to launch a popup?) New dom nodes aren't initialized automatically (load some new HTML via ajax, and your popups won't work without manually hooking them up)


Resolving these problems informed the javascript framework that we've built for 4.0. It all ultimately comes down to organizing code better and making the lives of developers easier (ours and yours!).

The solution

Our solution has been to build a framework which is modularized and heavily event-driven. In most cases, modules never call each other directly, but instead communicate with events which may (or may not) be responded to by another module. More on this later.

The framework breaks down into four types of module:
Widgets - interface widgets like popups, tooltips and menus Utility modules - things like cookie handling or URL manipulation Controllers - modules which work on a particular dom node. More on these later. Models - modules which handle data. In the vast majority of cases this is simply fetching data from the server.

There's also 'interfaces', which are third party scripts like CKEditor, jQuery plugins and so forth. They aren't part of the framework, so I won't discuss them here.


The groundwork

Before getting to specific types of modules, we needed to lay the groundwork. Javascript 4.0 is modularized, with only a single global variable (ips) being created in the page. All other scripts are defined as modules, whether they are interface widgets, utilities or anything else. A module is defined as a function which returns an object containing public methods (the revealing module pattern, if you're interested). Here's an example module:
;( function($, _, undefined){ "use strict"; ips.createModule('ips.myModule', function () { // Private methods var _privateMethod = function () { }; // Functions that become public methods var init = function () { }, publicMethod = function () { }; // Expose public methods return { init: init, publicMethod: publicMethod } }); }(jQuery, _));
This pattern works well for our purpose, because it enables a module to contain private methods for doing internal work, while exposing only those methods which should be public to the outside world.

This example module could then be used like so:

ips.myModule.publicMethod();
So this keeps everything neatly organized, and ensures variables don't leak into the global scope (which we want to avoid at all costs). When the document is ready, the module is automatically initialized (though you can also have functions that execute before DOMReady if necessary).


Interface widgets

It's fair to say that interface widgets make up a large proportion of the JS used in our software - a web app such as ours has an intrinsic need for popups, menus, tooltips and so on. As mentioned above though, the big hinderance in 3.x is that these widgets have to be created manually (or, in a few simple cases, a special classname is added to an element to initialize it). This feels unnecessary when all the developer wants to do is show a simple widget that is otherwise standard.

To alleviate this hassle, interface widgets in 4.0 all support a data API. What this means is that any widget can be created simply by adding some parameters to an HTML element, and specifying some options. Need a dialog box that loads a remote page? Simply do:

<a href='...' data-ipsDialog data-ipsDialog-title='My dialog' data-ipsDialog-url='http://...'>Click me to open a dialog</a>
Or if you need a hovercard, just do:

<a href='...' data-ipsHover>This will launch a hovercard</a>
We already have around two dozen widgets built, covering everything from dialogs, menus and tooltips, to keyboard navigation, tab bars and autocomplete - all supporting initialization with data attributes.

Building your own widgets is easy - they are built as a module, and then simply define some settings to be accepted. The widget module does the rest. They can either be initialized when they're first seen in the dom (which you'd want for something like an image slider widget), or when an event occurs (such as hovering on a link, in the case of hovercards). Whenever new content is loaded into the page, widgets will be found and initialized automatically, too.

Most widgets emit events when certain things occur - when we get to Controllers, you'll see why that is useful.


Utilities

Utilities are simple modules that don't need much discussion. They simply provide methods which do something useful - for example, fetch/set a cookie, write to the user's local browser database, or handle timestamps.


Controllers

Controllers are the meat of the application. Whereas interface widgets are used (and reused) as dumb tools on a page, controllers provide the logic for particular elements, sections and pages. They would, for example, handle the interactions in the topic listing, or the interactions with a post. Notice the word interaction - controllers are specifically designed to deal with events on the page. In fact, that's almost all they do!

Controllers are initialized by specifying the controller name on an element, like so:

<div id='topic_list' data-controller='forums.topicList'> </div>
This div becomes the controller's scope. The controller can manipulate content inside the div, watch for events, and so on.

Controllers, in general, should be as specific and succinct - so simply specifying a page-wide controller then handling everything inside it is discouraged. If we take the topic list in forum view as an example:

<div id='topic_list' data-controller='forums.topicList'> <ul> <li data-controller='forums.topicRow'> ... </li> <li data-controller='forums.topicRow'> ... </li> <li data-controller='forums.topicRow'> ... </li> </ul> </div>
Each topic row might specify the forums.topicRow controller which handles locking, pinning, or marking that topic. The topic list itself might specify the forums.topicList controller, which handles sorting and loading more topics. By doing it this way, controllers become responsible only for a specific portion of the functionality, which keeps them lean and simple.


Controllers are entirely decoupled and cannot reference each other - which is by design, given that a controller is only interested and responsible for its own scope. To communicate, events are used. A controller can trigger events on the page, which other controllers, widgets and models might respond to (and in turn emit their own events).

Continuing the example above, let's assume one of our topic rows is being deleted. The forums.topicRow controller handles removing the HTML from the DOM, but it doesn't care what happens after that - it's not its responsibility. However, it emits a deletedTopic event to let the page know. The forums.topicList controller sees this event, and because it does care, it loads a new topic entry into the list. By using events like this, we can build interfaces that respond fluidly to user interactions while still maintaining separation of concerns.

So, how does a controller deal with events? Because we're using jQuery, event handling in controllers piggy-backs with the on and trigger methods. In the controller's initialize method (which is specifically for setting up event handlers), you simply do:

this.on( 'menuItemSelected', '#menuid', this.handleMenuClick );
Usually when setting up events in an object using jQuery, you need to use $.proxy to properly control the scope of this, but in controllers, this is handled for you automatically - you just specify the method name.

Notice the event we're observing here - menuItemSelected. This is an event that the ui.menu widget emits, and it illustrates how widgets and controllers can interact. Controllers can watch for events from widgets, then do something with the information given, all without ever directly referring to each other.

Triggering an event is similar:

this.trigger( 'doSomething', { color: 'yellow', size: 'big' });
This is the same syntax as jQuery's own trigger, except that the controller will ensure the parameters object is passed between different event handlers in the same chain. This will hopefully be clearer when you get your hands on it.


Models

Models are quite similar to controllers (they also use the special on and trigger methods), but their only purpose is to handle data. By decoupling data handling from controllers, we can centralize data getting/setting so that any controller can use it.

Let's say we have a user model, which handles data for the current user. It might have event handlers for adding a friend, for example, so when it sees the event addFriend, it handles it appropriately. Let's also assume we have a controller on each post in a topic, there's three posts, and that the controllers are observing the click event on the 'add friend' button. Here's the sequence of events:

(controller1) click 'add friend button'
(controller1) emit 'addFriend' event
(user model) adds a friend via ajax
(user model) emits 'addedFriend' event
(controller1) updates friend icon
(controller2) updates friend icon
(controller3) updates friend icon

Even though it was controller1 that requested that the model adds a friend, all controllers respond to the event the model emits and updates the friend icon in its own post. This again shows the power of using events as the primary communication system - anyone can respond, and the caller doesn't have to deal with maintaining associations.



Conclusion

So that's about it - the new JS framework in IPS4. Hopefully this in-depth post has covered everything you need to know at this stage. You'll be pleased to know that most of the framework and widgets are already documented, and that will be available when IPS4 hits beta.

Do note that everything covered here is subject to change or removal, as usual in our development blogs.

If you have any questions, feel free to ask!


  • 14,351 views
Sending email is an important part of almost any web software available today. Social Suite applications have varying needs for sending email. Examples include allowing users to confirm their email address during registration, advising users when there are updates regarding content they are following, and bulk mailing your member base to alert them to something important regarding your site. While functionally the email handler has not significantly changed in 4.0, the process has been vastly simplified, allowing you to send more consistent emails with far less code than in the past.

Building and sending emails

When sending email in the Social Suite, there are two primary types of email you might need to send: an email based on a pre-existing template that you swap out variables in, and a completely custom email where you define the body specifically for the current email being sent. Both of these scenarios are accounted for. First, let's take a look at the simpler and more common scenario where you send an email based on a template.

IPSEmail::buildFromTemplate( 'core', 'admin_spammer', array( $this ) )->send( IPSSettings::i()->email_in );
Here, we are building an email from a template ("admin_spammer" in the "core" application, more on that in a moment), passing the current object in for purposes of replacing out variables in the template, and then calling the send method to actually send the email, passing the recipient to it. The buildFromTemplate() method is a factory method that will create a new object of type IPSEmail. When calling the send() method, you can optionally pass a single email address to send the email too, an array of email addresses to send the email to, or a single instance of IPSMember or an array of IPSMember instances to send the email to. The library takes care of the rest.


The other type of email you may need to send is a completely custom email where you define the entire body without using a template. Bulk mails are a good example of this type of email. You can easily send these types of emails as well.

$email = IPSEmail::buildFromContent( $subject, $htmlBody, $plaintextBody ); $email->from = "[email protected]"; $email->send( "[email protected]" );
Here we call the factory method buildFromContent(), passing in the email subject, the HTML email body, and the plain text email body. You can optionally omit the plain text email body and it will be generated for you automatically.

We then specify the from address (which defaults to the suite's outgoing email address), and finally we call the send() method, passing in an email address.

While it is not recommended, you can create an email object without specifying either email body parameter, and specify these later if you have a need.


Further control and options

Beyond what is outlined above, you have fine-grain control over the details of the email being sent. The following example outlines more of the possible values you can specify when necessary

$email = IPSEmail::buildFromContent( "Hello world" ); $email->from = "[email protected]"; $email->fromName = "John Doe"; $email->subject = "Changed my mind to hello planet"; $email->useWrapper = FALSE; $email->headers['X-Mailer'] = "My cool app"; $email->headers['Priority'] = 3; $email->setBody( array( 'html' => "<html><body><b>My email</b></body></html>", 'plain' => "My email" ) ); $email->send( array( "[email protected]", "[email protected]" ), array( "[email protected]" ), array( "[email protected]" ) );
Most of the above should be self-explanatory. Just a few quick notes:
You can set the $useWrapper property to FALSE if you do not wish for the email to be wrapped in the standard default HTML and plaintext email wrappers. The wrappers primarily define the HTML structure (doctype, html tag, etc.) and also allow for automatic definition of the unsubscribe link. You can call the $unsubscribe property to manually define an unsubscribe link and the text to display. Alternatively, you can call the "buildUnsubscribe()" method (see below) to do this automatically and consistently. There is no point in doing this if you do not use the wrapper, however, which is why it is omitted in the example above (in this case you should manually include an unsubscribe link if appropriate). The first parameter passed to the send() method is the to addresses. The second is the CC addresses, and the third is the BCC addresses, if necessary.



Automatically handling an unsubscribe link

Links to handle unsubscribing from emails can be accommodated automatically with emails sent through the email handler. For bulk emails, the following method is called
$email->buildUnsubscribe( 'bulk', IPSMember::loggedIn() );
The first parameter is the 'type', which is 'bulk' in this case. The second parameter when specifying an unsubscribe link for a bulk email should be the member who is being emailed, as their personal details are used to generate a unique key allowing one-click unsubscribing.

When sending notification emails, you still call buildUnsubscribe() with the following parameters

$email->buildUnsubscribe( 'notification', 'my_notification_key' );
Instead of a one-click unsubscribe link being included, a link pointing to the user's notification preferences area will be included instead, highlighting the notification preference that caused the email to be sent.

Unsubscribe links are only automatically included in the outgoing email if you (1) call this method, and (2) use the default wrappers.


Email templates

While we will touch on this further in a future blog entry, email templates are now defined in a similar manner to HTML templates, and there is one template for the HTML version of the email and one template for the plaintext version of the email. This allows us to better tailor the email being sent dependent upon the content-type in order to provide the nicest experience for the user. The email subject is a language string based upon the email template name and is determined automatically by the email library when buildFromTemplate() is called.


Wrapping Up

While the email library still provides plenty of control to accomodate almost any scenario encountered within the Social Suite, for day to day needs the interface to send emails has been vastly simplified in order to provide a more consistent experience for both the user and the developers integrating applications within the Suite. We hope you find these small changes will make your jobs easier, without losing the control you have presently.
  • 5,871 views
I'd like to introduce two new areas we've been working on. These new areas are designed to support our developer community, while making it easier for our clients to get their custom projects taken care of.

Projects
http://community.invisionpower.com/resources/projects

The first new area is Projects. When you have a custom project for which you need a developer/designer, this new area will allow you to gather responses from developers interested in working with you.

Post your project details, choose an approximate budget and date, if applicable, and developers can then register their interest in the project. From there, you can contact developers to help you decide which to go with.

Projects you post will be open for responses for 30 days, after which they'll be closed. If you agree to work with a developer before that time, you can mark it as completed by clicking the Accept link next to the chosen developer's response. You'll be notified when new developers respond to your project, too.

IPS won't be involved in the communication between you and a developer in any way, so it's up to you to agree project details and exchange payment, if necessary, before work starts.

We hope this will become a handy tool to help match up customers and developers on custom projects. In time we'll add more features, such as the ability to review customers/developers when a project is complete.


Developer Profiles
http://community.invisionpower.com/resources/developers

The second new area is Developer Profiles. This new area gives developers a place to present themselves to potential customers. When a developer creates a profile, we'll automatically build a page for them that pulls in their Marketplace information, and gives them a way of highlighting one of their contributed files and a place to write about themselves.

As a customer, you can browse the listings to check out the developers in our community. If you've worked with a developer or just like their files, you can Recommend them by clicking the link on their page.

If a developer with a profile responds to a Project, we'll link their name to their developer profile so that you can find out more about them.



We hope these two new features will help foster more growth in the development community, making it easier for customers to find developers, and giving developers a central place to find potential work.

Check them out - and if you have any feedback, please do feel free to share it. We've already made many changes based on the feedback from our preview to developers, so now it's your turn :smile:

  • 9,241 views
Our proprietary Spam Service which was launched in 2009 has been a very popular feature for IP.Board license holders. This service (which is included at no additional cost for all active license holders) is queried during account registration on your IP.Board installation and will respond to the query with a flag to indicate the likelihood that the registration might be a spammer. You can control how your site should react to the various responses the service may return in the admin control panel, and combined with other anti-spam tools in IP.Board you can help prevent spam registrations from occurring on your site.

While the system is constantly "learning" and blocking new spam signups, we have performed some updates to the service recently that we feel will help the system respond even quicker and more reliably to spam account registrations.


First, Some Stats

For those interested, we have some interesting statistics to share with you.

To date, the spam service has responded to over 58 million requests! The service has responded to almost 163,000 requests in the last 24 hours alone, and the service continues to respond to between 5,000 and 10,000 more requests day over day.

A total of over 23 million user registrations have been blocked (i.e. a status code of 3 or 4 was returned by the spam service) to date. That's 23 million spammers we've helped you prevent from disrupting your community.


Quicker Responses

With the recent updates we performed, the system will more quickly respond to new spammer accounts than it did previously. It is important that the system do not treat a single report of a spammer as a permanent block on that account of course, however we identified several areas where the algorithms used could be tweaked to more quickly identify potential spammers and have performed these changes. Additionally, by use of decaying flags (treating newer reports with a higher priority than older accounts), the system can more quickly respond to new spammer threats.


Project Honeypot Integration

We have integrated our spam service with the popular Project Honeypot service. This means that all account registrations are checked through Project Honeypot, and the threat score that is returned from this service is used to help determine the likelihood of the IP address being associated with a spammer.


Stop Forum Spam Integration

In addition to integration with Project Honeypot, we have integrated the IPS Spam Service with Stop Forum Spam. Our spam service will check the Stop Forum Spam email address and IP address databases and use any information found here to help weight and score the likelihood that the registration is coming from a spammer.

It is important to note that we do not rely directly on the Stop Forum Spam data to determine a spammer status, but are instead using the data from this service to help weight the overall score based on all of the flags we have available.


Enterprise Spam Mitigation

If you are in a load balanced or cloud environment, you may wish to take advantage of this new offering which allows calls to the spam service from multiple origins using the same license key. Additionally, this service allows greater control over your spam mitigation service including weighting algorithm preferences and customized blacklists and whitelists. This addon service is available for $100/6 months. For more information, please contact our sales department.



Collectively we feel the changes we have made to the service will benefit all IPS customers who are making use of our IPS Spam Service. We hope these changes help your community fight the threat of spam more rigorously and more reliably than ever before.
  • 14,125 views
To round up our previous blog entries on the post editor in IPS Social Suite 4.0, there's just a few extra features not previously mentioned to show off.



@mentions

@mentions are a common feature on social media sites like Twitter and Facebook. If you type an @ symbol and then start typing the name of a friend, an autocomplete menu shows so you can quickly then click on the user and they'll receive a notification that they've been mentioned. In 4.0 you can do exactly this to mention any user.




Automatic Saving

Currently, when you're typing a post, every 2 minutes the content of the post is saved, so that if you accidentally navigate away from the page, your post content can be recovered. The content is saved by making an AJAX request.

In 4.0, we've rewritten this to use HTML5 web storage. This unloads this work to the browser, meaning no call needs to be made to the server. Because this is much more efficient, the save can be done much more frequently (every few seconds). This makes the autosave feature much more useful.

In addition, we've expanded the feature to support attachments. So if you've uploaded files, these too will be automatically recovered. Essentially if you're in the middle of typing a post and you refresh the page, everything will reappear exactly as you left it.


HTML Posting

If you allow some users (like administrators) to post arbitrary HTML, they will see an additional "Source" button on the editor. When clicked, this will show them the raw HTML for the post and they can manipulate it here


  • 19,002 views
Introduction

Joining my previous entries about content and uploading features in post editor in IPS Social Suite 4.0, I'd like to take you through the customisation features on the editor.


Toolbar layout

The buttons that appear on the toolbar are completely customisable in 4.0 and you can set different layouts for desktop, tablet and mobile (so that you don't show more buttons than the device can show).

This is what the management screen looks like:

(This is an unfinished design - the tabs won't be be like that in the final version.)

To move a button you just drag and drop. The buttons on the right allow you to add more rows or separators.


Clicking on a button brings up a dialog where you can adjust where and to whom it shows:



Adding Buttons

There are two ways to add a button to the editor.

The easiest way is to install a CKEditor plugin. CKEditor has loads of plugins, and installing is as easy as uploading the zip file from their site. Here's a screenshot of the symbol plugin being used:


The second way is similar to how custom BBCode currently works, you specify the HTML code to be added when the user clicks on the button. Manually created buttons can optionally have a dialog popup to ask for an option.


Design

Just as you can install CKEditor plugins by uploading the zip file, you can do exactly the same with CKEditor skins to change the design of the editor.
You then simply set for each skin on your community which CKEditor skin to use for it.


BBCode

Though no features in IPS4 insert BBCode-style tags into the editor (like is currently done for attachments, etc.) users can still type BBCode into the editor and it will work fine.

We've rewritten how BBCode is parsed to be much more secure and reliable and produce more standards-compliant HTML (for those who are interested, it parses the post content into a DOM Document and examines only the text nodes for BBCode tags, then either splits the nodes surrounding it and inserts one for block-level elements, or wraps all subsequent text nodes in the appropriate formatting element until the end BBCode is found).

The benefit to this is that there now no longer needs to be a "BBCode mode" - you can type BBCode straight into the editor, even complicated stuff like lists spanning multiple lines, and it comes out looking great.

The downside to this approach is that custom BBCodes can no longer be added through the Admin CP. However, as mentioned above, we now have the ability to add custom buttons to the editor which work in a much more intuitive way, and can do everything that custom BBCodes could and more. For those who really want to be able to add the ability for custom BBCode, we've isolated the method that returns the supported BBCode (and information needed to parse them) into a specific method so that custom BBCode can be added with a very simple hook specific to that purpose.


Conclusion

There's still one more blog entry to go in our series on the editor. To finish up I'll be showing off some cool special features including how you can post using regular HTML.
  • 24,943 views
Introduction

In the last blog entry I introduced some of the features in the post editor in IPS Social Suite 4.0. In this blog entry I'd like to show you the uploading features in the editor.


Using the "Image" and "Attachment" dialogs

Along the bottom of the editor there are two buttons that deal with uploading files: image and attachments. Both present a dialog which looks like this:

We decided to keep both an images and an attachments dialog as users wanting to insert an image will naturally look for the "Image" button - if however, you upload an image to the attachments dialog, it will work completely as expected.

The upload panel here is based on HTML5 which supports drag and drop uploading, if your browser doesn't support this, it will use Flash, Silverlight or Google Gears if you have any of those installed, and if not it will fallback to a HTML4 & JavaScript implementation (none of these support drag and drop, but instead you click the "Choose Files" button just as you do now - the label in the box will change to reflect this).

Uploaded files then show below the box (images will get a preview), and you can click on any to add them into the editor, or click the "Insert All" button. When you insert an attachment into the editor, it displays either the image if it's an image, or a link if it's anything else, just as it will actually appear in the post (rather than the current "[attach=XXX]" tag).

You can also of course delete the attachment, which will automatically remove it from the editor if you've already inserted it.

Video Demonstration


Quick drag-and-drop

In addition to interacting with the panels, if you're using a supported browser, you can drag and drop straight into the editor. It will automatically figure out whether the uploaded file(s) are images or other files and add them to the appropriate panel automatically.

Video Demonstration


Image URLs

In the image panel, there is an additional "From URL" tab which allows you to insert an image from a URL, as you type the URL a preview is shown, and you can optionally link to the image.

Video Demonstration


My Files

In IP.Board currently, there is a "My Media" button which allows you to insert content submitted either in other posts or elsewhere in the community (images in IP.Gallery or files in IP.Downloads for example) into the editor. In 4.0, this feature is found in the images and attachments dialogs.

Just with normal attachments, the content is inserted as it will be shown rather than the current "[sharedmedia=XXX]" tag.


Conclusion

Please let us know what you think of the uploading features in the comments. Remember though that we're only half way through our series on the 4.0 editor. In my next blog entry I'll be talking about customising the editor and the place of BBCode.
  • 18,939 views
Introduction

The post editor is undoubtably one of the most frequently used features of the IPS Social Suite as it's the way users submit content to your community and functionality has evolved dramatically from the early days of forum software which consisted of a plain textbox in which users would type BBCode into the feature-rich WYSIWG (What You See Is What You Get) editors prevalent on the web today.

For the IPS Social Suite 4.0 we really wanted to focus on making the editor as good as it can be: feature-rich, intuitive to users and highly customisable.

Over the next 4 blog entries I'm going to cover the functionality of the editor and related features (attachments, emoticons, etc.) and the customisation options that will be available in 4.0. The theme of each blog entry will be: Here's a screenshot of what it looks like (by default, on a desktop, more on that later): Quotes A feature added to IP.Board in the 3.x series was "visual quotes" - when adding a quote the box shows directly in the editor as it will show in the post, rather than as just normal text with a quote BBCode wrapped around. In 4.0 we've rewritten how this works to use a CSS3 based solution rather than JavaScript to keep the citation header attached to the quote, which makes the feature much more reliable and easy to use when splitting quote boxes, moving the cursor before or after the quote box or dealing with embedded quotes. Code We've also rewritten how code can be inserted into the editor to be more reliable. Adding code now initially brings up an editor which supports syntax highlighting: And after inserting, you'll see the code exactly how it will be displayed after posting and you can interact with it exactly how you'd expect: Spoilers Spoilers are now also visual (though of course they're not true WYSIWYG as you need to be able to see what you're typing) and a built-in feature enabled by default. When you click the spoiler button you'll be given a darkened box in which to type the content: The spoilers display in the post as the same coloured box with a "click to view" message. When clicked, the dark box fades away revealing the content underneath. Emoticons We've completely overhauled emoticon management in 4.0. You can now create multiple "groups", drag and drop to reorder and quickly upload loads of emoticons at once. When you click the emoticon button in the editor, you'll see a popup with an overview of all the groups. If your community only uses the default emoticons, this is big enough to show all the emoticons, however, if you like to install lots of emoticons, you can select any group from the dropdown menu to show all emoticons in that group, or use the search bar to find a particular emoticon. In addition, your most recently used emoticons will show right at the top. Embedded Media In 3.0 we introduced a media BBCode tag. By wrapping a URL to media such as YouTube or Vimeo videos in media BBCode tags, the correct embed code would automatically be worked out and embedded into the post. In later versions, we added automatic embedding support, so just pasting the URL into the post would cause the video to be embedded. This still works exactly the same in 4.0 with a couple improvements: Previews If you type the URL to a supported media in the "Link" dialog, a preview will be shown in the dialog - you can choose to insert the embedded media or just a regular link. If you insert the embedded media, it will show directly in the editor. oEmbed Currently URLs which gets picked up for media embedding and the output they produce are managed in the Admin CP. The idea was that this would allow admins to add support for additional services, however, it has caused some problems when for example, YouTube has added new URL formats or changed their embed code and they don't work on most communities until we release an update to change the default record for YouTube. is an API which allows you to make a call to the provider giving them the URL and they respond with the best embed code to use, if that URL can be embedded. In 4.0, if a URL for YouTube, Flickr, Vimeo, College Humor or Hulu is inserted, we make a call to their oEmbed provider to obtain the embed code, so it's always up to date. Conclusion This is just the first in our series of blog entries on the editor in IPS4. Please do let us know what you think in the comments, but remember there's much more to come!
[*]Content [*]Uploads [*]Customisation and BBCode [*]Special features



































Video Demonstration





Video Demonstration












Video Demonstration






oEmbed




  • 24,965 views
The following applications are available for beta testing:
IP.Board 3.4.5 IP.Blog 2.6.3 IP.Content 2.3.6 IP.Chat 1.4.4 IP.Downloads 2.5.4 IP.Gallery 5.0.5 IP.Nexus 1.5.8 IP.Calendar 3.3.4


This round of maintenance updates represents updates to all of our applications. At this point in time, there are zero open bug reports in our tracker (that are not flagged to be resolved in a future major version). We would like to encourage all interested users to perform as much testing of these apps as possible.

We will be upgrading our company forums early this coming week.

Please report any bugs you find with the beta to our bug tracker.

Please pay particular attention to the following areas:The editor, both within IP.Board (topics/posts) and in other areas (such as IP.Content articles, IP.Blog entries, etc.). Pay attention both to the editor itself (i.e. when typing out and formatting your post, and toggling the editor back and forth) and the final post that is submitted. Rebuilding posts, specifically upon upgrading from an older version of the software. Pay attention specifically to quotes to be sure they display correctly. Any IP.Nexus functionality that has to do with grouped renewals. Cancelling packages where renewals are grouped, reactivating those packages, changing those packages, etc.


If you find a bug, please be certain to report it to the bug tracker. When doing so, be sure to include as much information as possible that will allow us to reproduce the issue, including what browser you are using, what version of PHP is running on your server, whether this was an upgrade or a fresh installation, and so on. Screenshots are often helpful.
As with all beta releases from IPS, these releases are not supported by our technicians until the official final releases are publicly available in our client center. Please do not upgrade your live installation using these betas, as you may find no path between these builds and the final releases that we put out. We recommended, instead, to create a copy of your live board as a test installation, and upgrade your test installation instead.

All customers with active licenses can download the betas at: http://community.inv...ower.com/qa.php

Thanks in advance! We look forward to your feedback.
  • 113,300 views
At IPS not only do we create market leading products, we also use them heavily ourselves. In doing so we often identify problems and needs in the software very early on. Two of the minor changes we've introduced with Nexus 1.5.8 are a direct result of management and support staff feedback during their use of the product internally.

Separating Outgoing Email Addresses

In the next release of IP.Nexus we've introduced the ability to set up separate outgoing email addresses for billing and support related notifications. these in turn are also separate from the default suite outgoing address. What this means in practice is that you can set up your community with a "noreply@" address where replies are not desirable and you want to avoid having to deal with bounced emails. Your billing notifications could be set up from "billing@" and ticket notifications from "support@". Replies can then be received via email and directed to the appropriate departments automatically.

Account Credit Increase Item

Businesses often have a primary contact and a separate billing contact. For accounting purposes, it is easier for them to add 12 months worth of account credit to the main account. Currently, in order to do this an invoice needs to be generated for a miscellaneous charge. The alternate contact has to then log in and pay this invoice then notify us that they've made payment. Finally, we have to look up the invoice and then manually add the credit to the account.

In IP.Nexus 1.5.8 we have added a new invoice item type in the ACP.



When an invoice containing this item type is paid, the customer's account credit will be automatically increased without further intervention from a staff member.

These types of real world usage scenarios and feedback are vital when developing all of the products in the IPS Suite. Your feedback as well as our internal usage help us to build products based on what our customers actually require rather than what an isolated group of developers may think they require.

Do you have suggestions for any other other workflow improvements such as these? However small or seemingly trivial you think they may be we are always interested in hearing ways in which we can make these tasks that little bit easier.

  • 6,952 views
We are pleased to announce an update to our popular converter application and with it support for conversions from vBulletin 5 Connect.

This release has been particularly focused on converter stability and performance improvements. At the time of writing there are 0 open bug reports in the bug tracker.

Performance Improvements

Whilst data integrity will always be our number one priority we do often review and apply performance improvements where appropriate.

In a previous release we introduced a feature that would significantly reduce the time taken when selecting data from the source software but this was only applied to a select few products. We are happy to report this change has now been applied across all converters. For the technically curious, this eliminates the need to use expensive LIMIT clauses with high offsets when converting forums with large amounts of posts.

Additionally we identified a few areas that would benefit from extra indexes particularly in the area of looking up relational content.

Software Specific Improvements

Support for vBulletin5 Connect is now available and the following items are converted - Permissions, Groups, Members, Passwords, Forums, Topics, Posts, Attachments, BBCode, Profile Fields, Emoticons, Moderators, Friends, Ignored Users, Reputation, Ranks, Warn Logs

The Ning converter has been greatly overhauled and now works much more reliably.

Considering switching to IPS?

Now is a great time to switch to IPS. We have a full community suite of products and a great resource community in the Marketplace. For a limited time through May 15 you can use the coupon code SWITCH at checkout to take 10% off your order. Feel free to email [email protected] with questions or post in our pre-sales forum to get feedback from other clients.

  • 6,115 views
The IPS Marketplace is the place to go for plugins, skins, language packs, full applications, and other resources provided by the IPS community. Some resources are free and some have a small fee. It's a great way to find ways to personalize your community and expand its functions.


Some updates...

I wanted to share some general statistics on the Marketplace (yes, I'm channeling Apple here). Yesterday we reached a great milestone:

Since its inception, we have paid out over $250,000 to contributors. Yes over a quarter million dollars has been paid out to those that sell resources in the Marketplace!

What's even more exciting is the growth we are seeing. In fact over 40% of that total payout was done just this year! Because I love any excuse to play with Excel here's a chart showing growth trend:



We also now have over 500 individual contributors that are providing both free and paid resources in the Marketplace.

Improvements coming soon

We are in the process of doing some cleaning up to make resources easier to find. Expect to see new categories based on what things do (moderation tools, promotion, utilities, etc.) rather than what they are (hook, mod, app, etc.) which we think will make finding resources for your community even easier.


A new ability to allow us to feature more than one resource at a time has already been launched.

Take a moment to browse the IPS Marketplace and see if there are any resources that would benefit your community. If you find one don't forget to thank the contributor!


  • 9,718 views
IPS is always looking for ways to assist our clients in monetization and promotion of their community traffic. After we saw so many clients successfully using VigLink on their community we decided to bundle it directly in the AdminCP under the Community Enhancements section. Since then many clients have reported success in using their service. Many clients have also since looked into other monetization options and have started to really grow.

Information from VigLink:



If you want to give it a try simply visit your AdminCP and click Community Enhancements to turn VigLink on and start earning. IPS does benefit from this relationship however we do not take any of your commission of course.
  • 12,072 views
Introduction

I previously wrote a blog entry about building tables in IPS Social Suite 4.0. Similar to tables, we also have trees. Trees in many ways look and behave similarly to tables, but can be distinguished mainly by the fact that trees show a collection of objects in a fixed order (often, though not always the order can be changed by the administrator) whereas tables can show data sorted however you like at the time. An example of trees would be the list of forums in IP.Board.

Trees vary quite significantly in their individual implementations, for example:
Objects in a tree often have parent/child relationships. Sometimes this relationship is between the same type of object (for example, forums in IP.Board or categories in IP.Downloads), sometimes the relationship is between different types of object (for example, applications and modules, where modules are always the child of applications) and sometimes it's a mix of both (for example, packages in IP.Nexus are always children of package groups, but package groups may or may not be children of each other). Objects in a tree can usually, but not always be reordered by the administrator. Objects in a tree can sometimes be enabled and disabled without being deleted, for example applications and modules. Sometimes individual objects can't when the rest can (for example, you can't disable the "Core" application). Trees usually have controls for adding, editing, assigning permissions, duplicating and deleting objects, though a subset of these controls, or additional controls may be available (for example, you can't edit the permissions on a package in IP.Nexus, but you can run a purchase report, which you can't do in any other trees).


For IPS Social Suite 4.0, I wanted to create a central class to create trees - currently we duplicate a lot of functionality, and all our trees display differently (in some places, quite radically so), but without loosing any of the flexibility necessary given the various differences in each implementation. Because trees are both more complicated and flexible than tables, the method for creating one might seem complicated - however, when you compare it to having to write every part manually (including all the JavaScript) as was the case in IP.Board 3, you'll be shaving hours off of development time.

There are two ways you can create trees in IPS Social Suite 4.0. The most common way is when each item in the table has a record in the database. There are occasions when this isn't the case (for example, the developer center displays trees created from JSON objects), and the Tree class can handle these, but in this blog entry I'm going to show you the more common method.

Just like I did for tables, I'm going to take you through how I programmed a real-world example, specifically the tree for custom profile fields.



Creating the classes

Custom profile fields are arranged into groups, so I'm going to start by creating a tree that shows the groups. To do this, I need to create a class for the custom profile field groups, I'll set this class to extend IPSNodeModel which is an abstract class that provides most of the functionality we need. IPSNodeModel in turn extends a class called IPSPatternsActiveRecord which provides functionality to make a class an Active Record (by that I mean, an object of the class corresponds to a record in the database).

In this class I need to define a few variables - I'll explain them below, but this is the code I'll write:
/** * Custom Profile Field Group Node */ class _Group extends IPSNodeModel { /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Default Values */ protected static $defaultValues = NULL; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'core_pfields_groups'; /** * @brief [ActiveRecord] Database Prefix */ public static $databasePrefix = 'pf_group_'; /** * @brief [ActiveRecord] ID Database Column */ public static $databaseColumnId = 'id'; /** * @brief [Node] Node Title */ public static $nodeTitle = 'profile_fields'; }
$multitons and $defaultValues are required by the IPSPatternsActiveRecord class. We don't need to do anything with them other than declare them. $databaseTable tells the IPSPatternsActiveRecord class what database table the records we need are in. $databasePrefix tells the IPSPatternsActiveRecord class the prefix used on all database columns. This isn't necessary, but since all the columns in my table start with "pf_group_", putting it here will save me typing it out every time. $databaseColumnId tells the IPSPatternsActiveRecord class which column contains the primary key. $nodeTitle tells the IPSNodeModel class what language string to use as the title on my page that shows the tree.



Now I need to create a controller to display my tree - the developer center will set up the structure of this for me, then I just fill in the name of the class I just created:
namespace IPScoremodulesadminmembersettings; /** * Profile Fields and Settings */ class _profiles extends IPSNodeController { /** * Node Class */ protected $nodeClass = 'IPScoreProfileFieldsGroup'; }
Now I have a page which looks like this:





Customising the rows

You'll notice I now have two rows (because I have two rows in my database), but both are blank. This is because the IPSNodeModel class doesn't know what to use for the record title. Let's fix that by adding a method to our class:

/** * [Node] Get Node Title * * @return string */ protected function get__title() { $key = "core_pfieldgroups_{$this->id}"; return IPSLang::i()->$key; }
That code might seem a bit confusing, but note:
$this->id gets the value of the "pf_group_id" column in the database. This is because the IPSPatternsActiveRecord class provides us with a __get method for retrieving the database row values. This is handy if we want to be able to modify the value returned for whatever reason, as we can override that method. We're retrieving the value for a language key rather than some kind of title field in the database. This is because in IPS Social Suite 4, data like this will be translatable, so if you have more than one language you can display different values depending on the user's language choice.


So now we have this (I've clicked the dropdown arrow so you can see it's contents):



Most of this is okay, but permissions aren't relevant for custom profile field groups, so let's get rid of that. The IPSNodeModel class has methods for checking if the user has permission to each button it displays (which by default check ACP restrictions, which I'll explain more about later) - we can simply override the method which checks for that button so it always returns false:
/** * [Node] Does the currently logged in user have permission to edit permissions for this node? * * @return bool */ public function canManagePermissions() { return false; }
And now it's gone:





Making the buttons work

The first buttons I need to make work are the add on the "root" row, and the edit on each row below that. I'm going to ignore the add button on each record row for now as that is for adding a child record and we haven't got to that yet - it will start working automatically when we add support for children.

These buttons will display a form. In a previous blog entry I talked about our form helper class. I'm going to use this to build the add/edit form.

To do this, I'll add two methods to my class to display the form and to save it's values - here they are:

/** * [Node] Add/Edit Form * * @param IPSHelpersForm $form The form * @return void */ public function form( &$form ) { $form->add( new IPSHelpersFormTranslatable( 'pfield_group_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_pfieldgroups_{$this->id}" : NULL ) ) ) ); } /** * [Node] Save Add/Edit Form * * @param array $values Values from the form * @return void */ public function saveForm( $values ) { if ( !$this->id ) { $this->save(); } IPSLang::saveCustom( 'core', "core_pfieldgroups_{$this->id}", $values['pfield_group_title'] ); }
Most of that should be self explanatory - however in the blog entry about forms I didn't mention the Translatable class. If you have one language installed, this will just display a normal text field, however, if you have more than one, it will show one for each language. It then returns an array, which we can pass to IPSLang::saveCustom() to save the values.

This is what our form might look like if I have several languages installed:


Or, more commonly, if I just have one:



Now the add and edit forms are working, but since these are very small forms (they only have one input field) I'd like them to display in a modal popup rather than take the user to a new page (if the user has JavaScript disabled, a new page will do). To do this, I just add a property to my class:

/** * @brief [Node] Show forms modally? */ public static $modalForms = TRUE;
Next we have the copy and delete buttons. These will actually work by themselves (the central class will handle copying and deleting the records from the database), however, since we have translatable values, we need to make sure those too are copied and deleted appropriately. To do this, I'll override the two methods which handle copying and deleting:

/** * [ActiveRecord] Duplicate * * @return void */ public function __clone() { $oldId = $this->id; parent::__clone(); IPSLang::saveCustom( 'core', "core_pfieldgroups_{$this->id}", IPSDb::i()->buildAndFetchAll( array( 'select' => '*', 'from' => 'core_sys_lang_words', 'where' => array( 'word_key=?', "core_pfieldgroups_{$oldId}" ) ), 'lang_id', 'word_custom' ) ); } /** * [ActiveRecord] Delete Record * * @return void */ public function delete() { parent::delete(); IPSLang::deleteCustom( 'core', 'core_pfieldgroups_' . $this->id ); }
Search

You'll notice that the system has automatically added a search box at the top of the table. In order to make this work, we need to add a simple search method:

/** * Search * * @param string $column Column to search * @param string $query Search query * @param string|null $order Column to order by * @return array */ public static function search( $column, $query, $order ) { if ( $column === '_title' ) { $return = array(); foreach ( IPSLang::i()->searchCustom( 'core_pfieldgroups_', $query ) as $key => $value ) { try { $return[ $key ] = self::load( $key ); } catch ( Exception $e ) { } } return $return; } return parent::search( $column, $query, $order ); }
Making the rows re-orderable

The last thing I need to do to finish the handling of groups is make it so we can drag and drop to reorder them. To do this I just add another property to my class telling IPSPatternsActiveRecord which column contains the order ID:

/** * @brief [Node] Order Database Column */ public static $databaseColumnOrder = 'order';

ACP Restrictions

It's important of course to make sure we honour ACP restrictions. The easiest way to do this is to create individual ACP restrictions for add/edit/delete (this can be done in the developer center) with a common prefix, and then specify like so:

/** * @brief [Node] ACP Restrictions */ protected static $restrictions = array( 'app' => 'core', 'module' => 'membersettings', 'prefix' => 'profilefieldgroups_', );
The system will now look for ACP restrictions with the keys "profilefieldgroups_add", "profilefieldgroups_edit" and "profilefieldgroups_delete" when performing those actions.


Children

Let's recap what we have so far with a video:
http://screencast.com/t/TUBuuYBSBON
Now that we have groups sorted, we're going to create another class for the actual fields which will show as children. The process is almost exactly the same as for groups. Since the process is the same, I won't go through the process step-by-step, but here is the class I've written if you're interested:
Field.php

The only changes are:
I've declared two additional properties specifying the class name of the parent ("IPScoreProfileFieldsGroup") and which column contains the parent ID. I've declared an additional method to fetch an icon for the row so we can see what type of field this is. Just like we overwrote canManagePermissions for groups, I've also overridden canAdd in the same way, as you cannot add children to profile fields.


Now all we need to do is link them up. To do this, I add a property to my group class telling IPSNodeModel the name of the class which contains children:
/** * @brief [Node] Subnode class */ public static $subnodeClass = 'IPScoreProfileFieldsField';
The system will now automatically change the behaviour of our page in the following ways:
When clicking on a group, it will expand out to show the fields under it. The search box will include fields as well as groups in its results. When clicking the "Add" button for a group, it will show the field to add a field to that group. When clicking the "Copy" button for a group, you'll have the option to copy children too or not. When clicking the "Delete" button for a group, you'll have the option to move children elsewhere or delete them too. (This is my favourite feature) In addition to being able to drag and drop fields to reorder, you can drag a field out of one group and into another.

Here's a video:
http://screencast.com/t/5fQwgle3EX
  • 13,893 views
A few weeks ago, I posted a blog entry mentioning a new feature in 4.0 which aims to make development of applications within the IPS Social Suite (both for us and third party modification authors) easier. We focussed on managing the database schema in that blog entry and I'd now like to take you through the other features.


Modules



Two tabs (one for admin modules and one for front modules) allow you to view all modules and sections in your application. You can add modules (which will both insert it into the database and create the relevant files in the filesystem), change the default section for a module (which previously required a defaultSection.php file) and create new sections.

When you're creating a new section, the form looks like this (this is for creating a section for an admin module):


The "Type" field controls the code that will be placed in the file created for the section - the options are:
"Blank" - which will create the class with no other logic, so the section will be blank. "Table" which will create a class with a boilerplate for displaying a table. "Node Controller" - which will create a boilerplate for displaying a tree of containers such as IP.Board forums, IP.Downloads categories, IP.Nexus groups, etc. We've not posted how this class works, but a future blog entry will give more details.

No matter which type you select, the system will automatically generate a file, with a basic class structure already filled in, including ACP restrictions checks, etc.

The "Menu Tab" field is admin specific and controls under which tab in the Admin CP the section should show (for example, we have some stuff from the "core" app under the "Look & Feel" tab). Previously, one would have to edit the menu.xml file for the module to add sections, and making sections appear under tabs other the default application tab was very difficult - the new system does it for you.

The "ACP Restriction" field is also admin specific and allows you to select an existing ACP restriction which will control who can see the section. It also has a special "Create Restriction" option, which will cause the system to create a restriction, and use that. Previously, one would have to edit the permissions.xml file to create restrictions and then assign them in the menu.xml file.

Of course - you can completely bypass this feature and manually create your module folders and section files, but the addition of this feature makes the process much quicker.



Admin CP Menu

This tab contains a graphical representation of the data which was previously stored in menu.xml files.








Admin CP Restrictions

This tab contains a graphical representation of the data which was previously stored in permissions.xml files.





Extensions

Extensions are ways in which applications interact with one another. Previously, you would drop extension files in your applications "/extensions" folder, though there wasn't much reasoning to the structure of the directory, it was difficult to know what extensions were available, and sometimes understanding an extensions requirements was difficult.

In 4.0, the extensions directory is more structured - the format is owner app > extension type > extension file (so admin/group_form.php for example, is now core/GroupForm/*.php) so this tab provides a GUI for managing your applications extensions. Applications can also specify a boilerplate file for an extension, so you can see what extensions are available, and clicking the "add" button will create a file with a basic structure to get you going.





Settings

In 4.0, developers have much more control over how settings are presented, rather than all being dumped in the central "System Settings" table. With this, much of the data that was previously needed in settings.xml is no longer required, so we've simplified the process of creating setting to just providing a key and a default value.





Versions

The versions tab shows all of the application's versions and the database queries that the upgrader will run when upgrading to that version. It's sort of a combination of the versions.xml file and the setup folder.




Queries are automatically added as you modify the database schema. Naturally you can also manually add queries, or specify a custom PHP script to run in that upgrade step.
  • 11,365 views

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.